/*
 * Copyright (C) 2017 Genymobile
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.genymobile.gnirehtet;

import android.net.VpnService;
import android.util.Log;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Forwarder {

    private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(3);

    private static final String TAG = Forwarder.class.getSimpleName();

    private static final int BUFSIZE = 4096;

    private static final byte[] DUMMY_ADDRESS = {42, 42, 42, 42};
    private static final int DUMMY_PORT = 4242;

    private final FileDescriptor vpnFileDescriptor;
    private final PersistentRelayTunnel tunnel;

    private Future<?> deviceToTunnelFuture;
    private Future<?> tunnelToDeviceFuture;

    public Forwarder(VpnService vpnService, FileDescriptor vpnFileDescriptor, RelayTunnelListener listener) {
        this.vpnFileDescriptor = vpnFileDescriptor;
        tunnel = new PersistentRelayTunnel(vpnService, listener);
    }

    public void forward() {
        deviceToTunnelFuture = EXECUTOR_SERVICE.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    forwardDeviceToTunnel(tunnel);
                } catch (InterruptedIOException e) {
                    Log.d(TAG, "Device to tunnel interrupted");
                } catch (IOException e) {
                    Log.e(TAG, "Device to tunnel exception", e);
                }
            }
        });
        tunnelToDeviceFuture = EXECUTOR_SERVICE.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    forwardTunnelToDevice(tunnel);
                } catch (InterruptedIOException e) {
                    Log.d(TAG, "Device to tunnel interrupted");
                } catch (IOException e) {
                    Log.e(TAG, "Tunnel to device exception", e);
                }
            }
        });
    }

    public void stop() {
        tunnel.close();
        tunnelToDeviceFuture.cancel(true);
        deviceToTunnelFuture.cancel(true);
        wakeUpReadWorkaround();
    }

    @SuppressWarnings("checkstyle:MagicNumber")
    private void forwardDeviceToTunnel(Tunnel tunnel) throws IOException {
        Log.d(TAG, "Device to tunnel forwarding started");
        FileInputStream vpnInput = new FileInputStream(vpnFileDescriptor);
        byte[] buffer = new byte[BUFSIZE];
        while (true) {
            // blocking read
            int r = vpnInput.read(buffer);
            if (r == -1) {
                Log.d(TAG, "VPN closed");
                break;
            }
            if (r > 0) {
                int version = buffer[0] >> 4;
                if (version == 4) {
                    // blocking send
                    tunnel.send(buffer, r);
                } else {
                    // see <https://github.com/Genymobile/gnirehtet/issues/69>
                    Log.w(TAG, "Unexpected packet IP version: " + version);
                }
            } else {
                Log.d(TAG, "Empty read");
            }
        }
        Log.d(TAG, "Device to tunnel forwarding stopped");
    }

    private void forwardTunnelToDevice(Tunnel tunnel) throws IOException {
        Log.d(TAG, "Tunnel to device forwarding started");
        FileOutputStream vpnOutput = new FileOutputStream(vpnFileDescriptor);
        IPPacketOutputStream packetOutputStream = new IPPacketOutputStream(vpnOutput);

        byte[] buffer = new byte[BUFSIZE];
        while (true) {
            // blocking receive
            int w = tunnel.receive(buffer);
            if (w == -1) {
                Log.d(TAG, "Tunnel closed");
                break;
            }
            if (w > 0) {
                // blocking write
                packetOutputStream.write(buffer, 0, w);
            } else {
                Log.d(TAG, "Empty write");
            }
        }
        Log.d(TAG, "Tunnel to device forwarding stopped");
    }

    /**
     * Neither vpnInterface.close() nor vpnInputStream.close() wake up a blocking
     * vpnInputStream.read().
     * <p>
     * Therefore, we need to make Android send a packet to the VPN interface (here by sending a UDP
     * packet), so that any blocking read will be woken up.
     * <p>
     * Since the tunnel is closed at this point, it will never reach the network.
     */
    private void wakeUpReadWorkaround() {
        // network actions may not be called from the main thread
        EXECUTOR_SERVICE.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    DatagramSocket socket = new DatagramSocket();
                    InetAddress dummyAddr = InetAddress.getByAddress(DUMMY_ADDRESS);
                    DatagramPacket packet = new DatagramPacket(new byte[0], 0, dummyAddr, DUMMY_PORT);
                    socket.send(packet);
                } catch (IOException e) {
                    // ignore
                }
            }
        });
    }
}

@@ -1296,12 +1296,15 @@
 E = 
-4096
+0x10000
 ;%0A%0A 

Use sufficient buffer size