eu.operando.operandoapp.service.ProxyService.java Source code

Java tutorial

Introduction

Here is the source code for eu.operando.operandoapp.service.ProxyService.java

Source

/*
 * Copyright (c) 2016 {UPRC}.
 *
 * OperandoApp 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.
 *
 * OperandoApp 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 OperandoApp.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Contributors:
 *       Nikos Lykousas {UPRC}, Constantinos Patsakis {UPRC}, Periklis Maravelias
 * Initially developed in the context of OPERANDO EU project www.operando.eu
 */

package eu.operando.operandoapp.service;

import android.app.AlertDialog;
import android.app.Service;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.wifi.WifiManager;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;

import org.apache.commons.lang3.StringUtils;
import org.littleshoot.proxy.ActivityTrackerAdapter;
import org.littleshoot.proxy.FlowContext;
import org.littleshoot.proxy.HttpFilters;
import org.littleshoot.proxy.HttpFiltersAdapter;
import org.littleshoot.proxy.HttpFiltersSource;
import org.littleshoot.proxy.HttpFiltersSourceAdapter;
import org.littleshoot.proxy.HttpProxyServer;
import org.littleshoot.proxy.impl.DefaultHttpProxyServer;
import org.littleshoot.proxy.impl.ProxyUtils;
import org.littleshoot.proxy.mitm.CertificateSniffingMitmManager;

import java.io.File;
import java.io.FileOutputStream;
import java.io.StringBufferInputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.SocketAddress;
import java.net.URLDecoder;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import eu.operando.operandoapp.MainContext;
import eu.operando.operandoapp.R;
import eu.operando.operandoapp.database.model.AllowedDomain;
import eu.operando.operandoapp.database.model.BlockedDomain;
import eu.operando.operandoapp.database.model.PendingNotification;
import eu.operando.operandoapp.database.DatabaseHelper;
import eu.operando.operandoapp.database.model.ResponseFilter;
import eu.operando.operandoapp.database.model.TrustedAccessPoint;
import eu.operando.operandoapp.util.MainUtil;
import eu.operando.operandoapp.util.ProcExplorer;
import eu.operando.operandoapp.util.RequestFilterUtil;
import eu.operando.operandoapp.wifi.model.WiFiDetail;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelHandlerInvoker;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.HttpChunkedInput;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.concurrent.EventExecutorGroup;

/**
 * Created by nikos on 8/4/2016.
 */

public class ProxyService extends Service {

    private static final String CustomHeaderField = "OperandoMetaInfo";
    private HttpProxyServer proxy;
    private int port = 8899;
    private MainContext mainContext = MainContext.INSTANCE;
    private ProcExplorer procExplorer;
    private DatabaseHelper db;
    private RequestFilterUtil requestFilterUtil;
    private String applicationInfo;

    private String[] locationInfo, contactsInfo, macAdresses;
    private String IMEI, phoneNumber, subscriberID, carrierName, androidID;

    @Override
    public void onCreate() {
        MainUtil.initializeMainContext(getApplicationContext());
        procExplorer = new ProcExplorer(mainContext.getContext());
        db = mainContext.getDatabaseHelper();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (proxy == null) {
            Log.e("OPERANDO", "-- PROXY ON START COMMAND--");
            HttpFiltersSource filtersSource = getFiltersSource();
            try {
                proxy = DefaultHttpProxyServer.bootstrap().withPort(port)
                        .withManInTheMiddle(new CertificateSniffingMitmManager(mainContext.getAuthority()))
                        .withAllowLocalOnly(false).withFiltersSource(filtersSource).withName("OperandoProxy")
                        .plusActivityTracker(new ActivityTrackerAdapter() {

                            /*
                            Get the package responsible for each request
                             */
                            @Override
                            public void requestReceivedFromClient(FlowContext flowContext,
                                    HttpRequest httpRequest) {
                                if (!MainUtil.isProxyPaused(mainContext)) {
                                    httpRequest.headers().add(CustomHeaderField,
                                            procExplorer.handleCommand(flowContext.getClientAddress()));
                                }
                            }
                        }).start();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return START_STICKY;
    }

    private HttpFiltersSource getFiltersSource() {
        return new HttpFiltersSourceAdapter() {
            @Override
            public HttpFilters filterRequest(HttpRequest originalRequest) {
                return new HttpFiltersAdapter(originalRequest) {
                    @Override
                    public HttpObject serverToProxyResponse(HttpObject httpObject) {
                        //check for proxy running
                        if (MainUtil.isProxyPaused(mainContext))
                            return httpObject;

                        if (httpObject instanceof HttpMessage) {
                            HttpMessage response = (HttpMessage) httpObject;
                            response.headers().set(HttpHeaderNames.CACHE_CONTROL,
                                    "no-cache, no-store, must-revalidate");
                            response.headers().set(HttpHeaderNames.PRAGMA, "no-cache");
                            response.headers().set(HttpHeaderNames.EXPIRES, "0");
                        }
                        try {
                            Method content = httpObject.getClass().getMethod("content");
                            if (content != null) {
                                ByteBuf buf = (ByteBuf) content.invoke(httpObject);
                                boolean flag = false;
                                List<ResponseFilter> responseFilters = db.getAllResponseFilters();
                                if (responseFilters.size() > 0) {
                                    String contentStr = buf.toString(Charset.forName("UTF-8")); //Charset.forName(Charset.forName("UTF-8")
                                    for (ResponseFilter responseFilter : responseFilters) {
                                        String toReplace = responseFilter.getContent();
                                        if (StringUtils.containsIgnoreCase(contentStr, toReplace)) {
                                            contentStr = contentStr.replaceAll("(?i)" + toReplace,
                                                    StringUtils.leftPad("", toReplace.length(), '#'));
                                            flag = true;
                                        }
                                    }
                                    if (flag) {
                                        buf.clear().writeBytes(contentStr.getBytes(Charset.forName("UTF-8")));
                                    }
                                }
                            }
                        } catch (IndexOutOfBoundsException ex) {
                            ex.printStackTrace();
                            Log.e("Exception", ex.getMessage());
                        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
                            //ignore
                        }
                        return httpObject;
                    }

                    @Override
                    public HttpResponse clientToProxyRequest(HttpObject httpObject) {
                        //check for proxy running
                        if (MainUtil.isProxyPaused(mainContext)) {
                            return null;
                        }

                        //check for trusted access point
                        String ssid = ((WifiManager) getSystemService(Context.WIFI_SERVICE)).getConnectionInfo()
                                .getSSID();
                        String bssid = ((WifiManager) getSystemService(Context.WIFI_SERVICE)).getConnectionInfo()
                                .getBSSID();
                        boolean trusted = false;
                        TrustedAccessPoint curr_tap = new TrustedAccessPoint(ssid, bssid);
                        for (TrustedAccessPoint tap : db.getAllTrustedAccessPoints()) {
                            if (curr_tap.isEqual(tap)) {
                                trusted = true;
                            }
                        }
                        if (!trusted) {
                            return getUntrustedGatewayResponse();
                        }

                        //check for blocked url

                        //check for exfiltration
                        requestFilterUtil = new RequestFilterUtil(getApplicationContext());
                        locationInfo = requestFilterUtil.getLocationInfo();
                        contactsInfo = requestFilterUtil.getContactsInfo();
                        IMEI = requestFilterUtil.getIMEI();
                        phoneNumber = requestFilterUtil.getPhoneNumber();
                        subscriberID = requestFilterUtil.getSubscriberID();
                        carrierName = requestFilterUtil.getCarrierName();
                        androidID = requestFilterUtil.getAndroidID();
                        macAdresses = requestFilterUtil.getMacAddresses();

                        if (httpObject instanceof HttpMessage) {
                            HttpMessage request = (HttpMessage) httpObject;
                            if (request.headers().contains(CustomHeaderField)) {
                                applicationInfo = request.headers().get(CustomHeaderField);
                                request.headers().remove(CustomHeaderField);
                            }
                            if (request.headers().contains(HttpHeaderNames.ACCEPT_ENCODING)) {
                                request.headers().remove(HttpHeaderNames.ACCEPT_ENCODING);
                            }
                            if (!ProxyUtils.isCONNECT(request)
                                    && request.headers().contains(HttpHeaderNames.HOST)) {
                                String hostName = ((HttpRequest) request).uri(); //request.headers().get(HttpHeaderNames.HOST).toLowerCase();
                                if (db.isDomainBlocked(hostName))
                                    return getBlockedHostResponse(hostName);
                            }
                        }

                        String requestURI;
                        Set<RequestFilterUtil.FilterType> exfiltrated = new HashSet<>();

                        if (httpObject instanceof HttpRequest) {
                            HttpRequest request = (HttpRequest) httpObject;
                            requestURI = request.uri();
                            try {
                                requestURI = URLDecoder.decode(requestURI, "UTF-8");
                            } catch (UnsupportedEncodingException e) {
                                e.printStackTrace();
                            }
                            if (locationInfo.length > 0) {
                                //tolerate location miscalculation
                                float latitude = Float.parseFloat(locationInfo[0]);
                                float longitude = Float.parseFloat(locationInfo[1]);
                                Matcher m = Pattern.compile("\\d+\\.\\d+").matcher(requestURI);
                                List<String> floats_in_uri = new ArrayList();
                                while (m.find()) {
                                    floats_in_uri.add(m.group());
                                }
                                for (String s : floats_in_uri) {
                                    if (Math.abs(Float.parseFloat(s) - latitude) < 0.5
                                            || Math.abs(Float.parseFloat(s) - longitude) < 0.1) {
                                        exfiltrated.add(RequestFilterUtil.FilterType.LOCATION);
                                    }
                                }
                            }
                            if (StringUtils.containsAny(requestURI, contactsInfo)) {
                                exfiltrated.add(RequestFilterUtil.FilterType.CONTACTS);
                            }
                            if (StringUtils.containsAny(requestURI, macAdresses)) {
                                exfiltrated.add(RequestFilterUtil.FilterType.MACADRESSES);
                            }
                            if (requestURI.contains(IMEI) && !IMEI.equals("")) {
                                exfiltrated.add(RequestFilterUtil.FilterType.IMEI);
                            }
                            if (requestURI.contains(phoneNumber) && !phoneNumber.equals("")) {
                                exfiltrated.add(RequestFilterUtil.FilterType.PHONENUMBER);
                            }
                            if (requestURI.contains(subscriberID) && !subscriberID.equals("")) {
                                exfiltrated.add(RequestFilterUtil.FilterType.IMSI);
                            }
                            if (requestURI.contains(carrierName) && !carrierName.equals("")) {
                                exfiltrated.add(RequestFilterUtil.FilterType.CARRIERNAME);
                            }
                            if (requestURI.contains(androidID) && !androidID.equals("")) {
                                exfiltrated.add(RequestFilterUtil.FilterType.ANDROIDID);
                            }
                        }
                        try {
                            Method content = httpObject.getClass().getMethod("content");
                            if (content != null) {
                                ByteBuf buf = (ByteBuf) content.invoke(httpObject);
                                String contentStr = buf.toString(Charset.forName("UTF-8"));
                                if (locationInfo.length > 0) {
                                    //tolerate location miscalculation
                                    float latitude = Float.parseFloat(locationInfo[0]);
                                    float longitude = Float.parseFloat(locationInfo[1]);
                                    Matcher m = Pattern.compile("\\d+\\.\\d+").matcher(contentStr);
                                    List<String> floats_in_uri = new ArrayList();
                                    while (m.find()) {
                                        floats_in_uri.add(m.group());
                                    }
                                    for (String s : floats_in_uri) {
                                        if (Math.abs(Float.parseFloat(s) - latitude) < 0.5
                                                || Math.abs(Float.parseFloat(s) - longitude) < 0.1) {
                                            exfiltrated.add(RequestFilterUtil.FilterType.LOCATION);
                                        }
                                    }
                                }
                                if (StringUtils.containsAny(contentStr, contactsInfo)) {
                                    exfiltrated.add(RequestFilterUtil.FilterType.CONTACTS);
                                }
                                if (StringUtils.containsAny(contentStr, macAdresses)) {
                                    exfiltrated.add(RequestFilterUtil.FilterType.MACADRESSES);
                                }
                                if (contentStr.contains(IMEI) && !IMEI.equals("")) {
                                    exfiltrated.add(RequestFilterUtil.FilterType.IMEI);
                                }
                                if (contentStr.contains(phoneNumber) && !phoneNumber.equals("")) {
                                    exfiltrated.add(RequestFilterUtil.FilterType.PHONENUMBER);
                                }
                                if (contentStr.contains(subscriberID) && !subscriberID.equals("")) {
                                    exfiltrated.add(RequestFilterUtil.FilterType.IMSI);
                                }
                                if (contentStr.contains(carrierName) && !carrierName.equals("")) {
                                    exfiltrated.add(RequestFilterUtil.FilterType.CARRIERNAME);
                                }
                                if (contentStr.contains(androidID) && !androidID.equals("")) {
                                    exfiltrated.add(RequestFilterUtil.FilterType.ANDROIDID);
                                }
                            }
                        } catch (IndexOutOfBoundsException ex) {
                            ex.printStackTrace();
                            Log.e("Exception", ex.getMessage());
                        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
                            //ignore
                        }

                        //check exfiltrated list
                        if (!exfiltrated.isEmpty()) {
                            //retrieve all blocked and allowed domains
                            List<BlockedDomain> blocked = db.getAllBlockedDomains();
                            List<AllowedDomain> allowed = db.getAllAllowedDomains();
                            //get application name from app info
                            String appName = applicationInfo.replaceAll("\\(.+?\\)", "");
                            //check blocked domains
                            //if domain is stored as blocked, return a forbidden response
                            for (BlockedDomain b_dmn : blocked) {
                                if (b_dmn.info.equals(appName)) {
                                    return getForbiddenRequestResponse(applicationInfo, exfiltrated);
                                }
                            }
                            //if domain is stored as allowed, return null for actual response
                            for (AllowedDomain a_dmn : allowed) {
                                if (a_dmn.info.equals(appName)) {
                                    return null;
                                }
                            }
                            //get exfiltrated info to string array
                            String[] exfiltrated_array = new String[exfiltrated.size()];
                            int i = 0;
                            for (RequestFilterUtil.FilterType filter_type : exfiltrated) {
                                exfiltrated_array[i] = filter_type.name();
                                i++;
                            }
                            //retrieve all pending notifications
                            List<PendingNotification> pending = db.getAllPendingNotifications();
                            for (PendingNotification pending_notification : pending) {
                                //if pending notification includes specific app name and app permissions return response that a pending notification exists
                                if (pending_notification.app_info.equals(applicationInfo)) {
                                    return getPendingResponse();
                                }
                            }
                            //if none pending notification exists, display a new notification
                            int notificationId = mainContext.getNotificationId();
                            mainContext.getNotificationUtil().displayExfiltratedNotification(getBaseContext(),
                                    applicationInfo, exfiltrated, notificationId);
                            mainContext.setNotificationId(notificationId + 3);
                            //and update statistics
                            db.updateStatistics(exfiltrated);
                            return getAwaitingResponse();
                        }
                        return null;
                    }
                };
            }
        };
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        proxy.stop();
        proxy = null;
        Log.e("OPERANDO", "-- PROXY KILLED--");
    }

    //region Responses

    private HttpResponse getPendingResponse() {
        String body = "<!DOCTYPE HTML \"-//IETF//DTD HTML 2.0//EN\">\n" + "<html><head>\n" + "<title>"
                + "Pending Notification" + "</title>\n" + "</head><body>\n"
                + "<h1>The page you are about to visit requires some sensitive information. Please see your pending notifications in Operando to allow or block such requests.</h1>"
                + "</body></html>\n";
        byte[] bytes = body.getBytes(Charset.forName("UTF-8"));
        ByteBuf content = Unpooled.copiedBuffer(bytes);
        HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_GATEWAY,
                content);
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
        response.headers().set("Content-Type", "text/html; charset=UTF-8");
        response.headers().set("Date", ProxyUtils.formatDate(new Date()));
        response.headers().set(HttpHeaderNames.CONNECTION, "close");
        return response;
    }

    private HttpResponse getAwaitingResponse() {
        String body = "<!DOCTYPE HTML \"-//IETF//DTD HTML 2.0//EN\">\n" + "<html><head>\n" + "<title>"
                + "Awaiting Response" + "</title>\n" + "</head><body>\n"
                + "<h1>The page you are about to visit requires some sensitive information. Please see your notification bar to allow or block such requests.</h1>"
                + "</body></html>\n";
        byte[] bytes = body.getBytes(Charset.forName("UTF-8"));
        ByteBuf content = Unpooled.copiedBuffer(bytes);
        HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_GATEWAY,
                content);
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
        response.headers().set("Content-Type", "text/html; charset=UTF-8");
        response.headers().set("Date", ProxyUtils.formatDate(new Date()));
        response.headers().set(HttpHeaderNames.CONNECTION, "close");
        return response;
    }

    private HttpResponse getBlockedHostResponse(String hostName) {
        String body = "<!DOCTYPE HTML \"-//IETF//DTD HTML 2.0//EN\">\n" + "<html><head>\n" + "<title>"
                + "Bad Gateway" + "</title>\n" + "</head><body>\n" + "<h1>Host '" + hostName
                + "' is blocked by OperandoApp.</h1>" + "</body></html>\n";
        byte[] bytes = body.getBytes(Charset.forName("UTF-8"));
        ByteBuf content = Unpooled.copiedBuffer(bytes);
        HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_GATEWAY,
                content);
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
        response.headers().set("Content-Type", "text/html; charset=UTF-8");
        response.headers().set("Date", ProxyUtils.formatDate(new Date()));
        response.headers().set(HttpHeaderNames.CONNECTION, "close");
        return response;
    }

    private HttpResponse getUntrustedGatewayResponse() {
        String body = "<!DOCTYPE HTML \"-//IETF//DTD HTML 2.0//EN\">\n" + "<html><head>\n" + "<title>"
                + "Untrusted Gateway" + "</title>\n" + "</head><body>\n"
                + "<h1>Gateway is not included in your trusted list.</h1>" + "<h2>Check Operando settings.</h2>"
                + "</body></html>\n";
        byte[] bytes = body.getBytes(Charset.forName("UTF-8"));
        ByteBuf content = Unpooled.copiedBuffer(bytes);
        HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_GATEWAY,
                content);
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
        response.headers().set("Content-Type", "text/html; charset=UTF-8");
        response.headers().set("Date", ProxyUtils.formatDate(new Date()));
        response.headers().set(HttpHeaderNames.CONNECTION, "close");
        return response;
    }

    private HttpResponse getForbiddenRequestResponse(String applicationInfo,
            Set<RequestFilterUtil.FilterType> exfiltrated) {
        String body = "<!DOCTYPE HTML \"-//IETF//DTD HTML 2.0//EN\">\n" + "<html><head>\n" + "<title>" + "Forbidden"
                + "</title>\n" + "</head><body>\n" + "<h1>Request sent by: '" + applicationInfo
                + "',<br/> contains sensitive data: " + RequestFilterUtil.messageForMatchedFilters(exfiltrated)
                + "</h1>" + "</body></html>\n";
        byte[] bytes = body.getBytes(Charset.forName("UTF-8"));
        ByteBuf content = Unpooled.copiedBuffer(bytes);
        HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN,
                content);
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, bytes.length);
        response.headers().set("Content-Type", "text/html; charset=UTF-8");
        response.headers().set("Date", ProxyUtils.formatDate(new Date()));
        response.headers().set(HttpHeaderNames.CONNECTION, "close");
        return response;
    }

    //endregion

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

}