= LinkedBlockingDeque()
fun write(data: ByteArray) {
if (data.size > MAX_PACKET_LEN) throw Error("Packet too large")
packetQueue.addLast(data)
}
fun shutdown() {
this.shutdown = true
}
override fun run() {
while (!this.shutdown && clientWriter.channel.isOpen) {
try {
val data: ByteArray = this.packetQueue.take()
try {
this.clientWriter.write(data)
} catch (e: IOException) {
Log.e(TAG, "Error writing $shutdown data.length bytes to the VPN")
e.printStackTrace()
// this.packetQueue.addFirst(data) // Put the data back, so it's resent
Thread.sleep(10) // Add an arbitrary tiny pause, in case that helps
}
} catch (ignored: InterruptedException) {
}
}
}
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/CloseableConnection.kt
================================================
package com.network.proxy.vpn.socket
import com.network.proxy.vpn.Connection
interface CloseableConnection {
/**
* 关闭连接
*/
fun closeConnection(connection: Connection)
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/Constant.kt
================================================
package com.network.proxy.vpn.socket
object Constant {
const val MAX_RECEIVE_BUFFER_SIZE = 65535
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/ProtectSocket.kt
================================================
package com.network.proxy.vpn.socket
import java.net.DatagramSocket
import java.net.Socket
interface ProtectSocket {
/**
* 保护Socket不受VPN连接的影响。保护后,通过该套接字发送的数据将直接进入底层网络,因此其流量不会通过VPN转发。
*/
fun protect(socket: Socket): Boolean
fun protect(socket: DatagramSocket): Boolean
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/ProtectSocketHolder.kt
================================================
package com.network.proxy.vpn.socket
import java.net.DatagramSocket
import java.net.Socket
/**
* ProtectSocket的持有者,用于在VPNService中获取ProtectSocket的实例
*/
class ProtectSocketHolder {
companion object {
private var protectSocket: ProtectSocket? = null
fun setProtectSocket(protectSocket: ProtectSocket) {
this.protectSocket = protectSocket
}
fun getProtectSocket(): ProtectSocket? {
return protectSocket
}
fun protect(socket: Socket): Boolean {
return protectSocket?.protect(socket) ?: false
}
fun protect(socket: DatagramSocket): Boolean {
return protectSocket?.protect(socket) ?: false
}
}
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/SocketChannelReader.java
================================================
package com.network.proxy.vpn.socket;
import androidx.annotation.NonNull;
import android.util.Log;
import com.network.proxy.vpn.Connection;
import com.network.proxy.vpn.TagKt;
import com.network.proxy.vpn.transport.protocol.IP4Header;
import com.network.proxy.vpn.transport.protocol.TCPHeader;
import com.network.proxy.vpn.transport.protocol.TCPPacketFactory;
import com.network.proxy.vpn.transport.protocol.UDPPacketFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.DatagramChannel;
import java.nio.channels.NotYetConnectedException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.AbstractSelectableChannel;
/**
* Takes a session, and reads all available upstream data back into it.
* Used by the NIO thread, and run synchronously as part of that non-blocking loop.
*/
class SocketChannelReader {
private final String TAG = TagKt.getTAG(this);
private final ClientPacketWriter writer;
public SocketChannelReader(ClientPacketWriter writer) {
this.writer = writer;
}
public void read(Connection connection) {
AbstractSelectableChannel channel = connection.getChannel();
if (channel instanceof SocketChannel) {
readTCP(connection);
} else if (channel instanceof DatagramChannel) {
readUDP(connection);
} else {
return;
}
// Resubscribe to reads, so that we're triggered again if more data arrives later.
connection.subscribeKey(SelectionKey.OP_READ);
if (connection.isAbortingConnection()) {
Log.d(TAG, "removing aborted connection -> " + connection);
connection.cancelKey();
if (channel instanceof SocketChannel) {
try {
SocketChannel socketChannel = (SocketChannel) channel;
if (socketChannel.isConnected()) {
socketChannel.close();
}
} catch (IOException e) {
Log.e(TAG, e.toString());
}
} else {
try {
DatagramChannel datagramChannel = (DatagramChannel) channel;
if (datagramChannel.isConnected()) {
datagramChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
connection.closeConnection();
}
}
private void readTCP(@NonNull Connection connection) {
if (connection.isAbortingConnection()) {
return;
}
SocketChannel channel = (SocketChannel) connection.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(Constant.MAX_RECEIVE_BUFFER_SIZE);
int len;
try {
do {
len = channel.read(buffer);
if (len > 0) { //-1 mean it reach the end of stream
sendToRequester(buffer, len, connection);
buffer.clear();
} else if (len == -1) {
// Log.d(TAG,"End of data from remote server, will send FIN to client");
Log.d(TAG, "send FIN to: " + connection);
sendFin(connection);
connection.setAbortingConnection(true);
}
} while (len > 0);
} catch (NotYetConnectedException e) {
Log.e(TAG, "socket not connected");
} catch (ClosedByInterruptException e) {
Log.e(TAG, "ClosedByInterruptException reading SocketChannel: " + e.getMessage());
} catch (ClosedChannelException e) {
Log.e(TAG, "ClosedChannelException reading SocketChannel: " + e.getMessage());
} catch (IOException e) {
Log.e(TAG, "Error reading data from SocketChannel: " + e.getMessage());
connection.setAbortingConnection(true);
}
}
private void sendToRequester(ByteBuffer buffer, int dataSize, @NonNull Connection connection) {
// Last piece of data is usually smaller than MAX_RECEIVE_BUFFER_SIZE. We use this as a
// trigger to set PSH on the resulting TCP packet that goes to the VPN.
connection.setHasReceivedLastSegment(dataSize < Constant.MAX_RECEIVE_BUFFER_SIZE);
buffer.limit(dataSize);
buffer.flip();
// TODO should allocate new byte array?
byte[] data = new byte[dataSize];
System.arraycopy(buffer.array(), 0, data, 0, dataSize);
connection.addReceivedData(data);
//pushing all data to vpn client
while (connection.hasReceivedData()) {
pushDataToClient(connection);
}
}
/**
* create packet data and send it to VPN client
*/
private void pushDataToClient(@NonNull Connection connection) {
if (!connection.hasReceivedData()) {
//no data to send
Log.d(TAG, "no data for vpn client");
}
IP4Header ipHeader = connection.getLastIpHeader();
TCPHeader tcpheader = connection.getLastTcpHeader();
// TODO What does 60 mean?
int max = connection.getMaxSegmentSize() - 60;
if (max < 1) {
max = 1024;
}
byte[] packetBody = connection.getReceivedData(max);
if (packetBody != null && packetBody.length > 0) {
long unAck = connection.getSendNext();
long nextUnAck = connection.getSendNext() + packetBody.length;
connection.setSendNext((int) nextUnAck);
//we need this data later on for retransmission
// connection.setUnackData(packetBody);
// connection.setResendPacketCounter(0);
byte[] data = TCPPacketFactory.createResponsePacketData(ipHeader,
tcpheader, packetBody, connection.getHasReceivedLastSegment(),
connection.getRecSequence(), (int) unAck,
connection.getTimestampSender(), connection.getTimestampReplyTo());
writer.write(data);
}
}
private void sendFin(Connection connection) {
final IP4Header ipHeader = connection.getLastIpHeader();
final TCPHeader tcpheader = connection.getLastTcpHeader();
final byte[] data = TCPPacketFactory.INSTANCE.createFinData(ipHeader, tcpheader,
connection.getRecSequence(), connection.getSendNext(),
connection.getTimestampSender(), connection.getTimestampReplyTo());
writer.write(data);
}
private void readUDP(Connection connection) {
DatagramChannel channel = (DatagramChannel) connection.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(Constant.MAX_RECEIVE_BUFFER_SIZE);
int len;
try {
do {
if (connection.isAbortingConnection()) {
break;
}
len = channel.read(buffer);
if (len > 0) {
buffer.limit(len);
buffer.flip();
//create UDP packet
byte[] data = new byte[len];
System.arraycopy(buffer.array(), 0, data, 0, len);
byte[] packetData = UDPPacketFactory.createResponsePacket(
connection.getLastIpHeader(), connection.getLastUdpHeader(), data);
//write to client
writer.write(packetData);
buffer.clear();
}
} while (len > 0);
} catch (NotYetConnectedException ex) {
Log.e(TAG, "failed to read from unconnected UDP socket");
} catch (IOException e) {
Log.e(TAG, "Failed to read from UDP socket, aborting connection");
connection.setAbortingConnection(true);
}
}
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/SocketChannelWriter.java
================================================
package com.network.proxy.vpn.socket;
import androidx.annotation.NonNull;
import android.util.Log;
import com.network.proxy.vpn.Connection;
import com.network.proxy.vpn.TagKt;
import com.network.proxy.vpn.transport.protocol.TCPPacketFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.NotYetConnectedException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.AbstractSelectableChannel;
/**
* Takes a VPN session, and writes all received data from it to the upstream channel.
*
* If any writes fail, it resubscribes to OP_WRITE, and tries again next time
* that fires (as soon as the channel is ready for more data).
*
* Used by the NIO thread, and run synchronously as part of that non-blocking loop.
*/
public class SocketChannelWriter {
private final String TAG = TagKt.getTAG(this);
private final ClientPacketWriter writer;
SocketChannelWriter(ClientPacketWriter writer) {
this.writer = writer;
}
public void write(@NonNull Connection connection) {
AbstractSelectableChannel channel = connection.getChannel();
if (channel instanceof SocketChannel) {
writeTCP(connection);
} else if(channel instanceof DatagramChannel) {
writeUDP(connection);
} else {
// We only ever create TCP & UDP channels, so this should never happen
throw new IllegalArgumentException("Unexpected channel type: " + channel);
}
if (connection.isAbortingConnection()) {
Log.d(TAG,"removing aborted connection -> " + connection);
connection.cancelKey();
if (channel instanceof SocketChannel) {
try {
SocketChannel socketChannel = (SocketChannel) channel;
if (socketChannel.isConnected()) {
socketChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
} else {
try {
DatagramChannel datagramChannel = (DatagramChannel) channel;
if (datagramChannel.isConnected()) {
datagramChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
connection.closeConnection();
}
}
private void writeUDP(Connection connection) {
try {
writePendingData(connection);
// Date dt = new Date();
// connection.connectionStartTime = dt.getTime();
}catch(NotYetConnectedException ex2){
connection.setAbortingConnection(true);
Log.e(TAG,"Error writing to unconnected-UDP server, will abort current connection: "+ex2.getMessage());
} catch (IOException e) {
connection.setAbortingConnection(true);
e.printStackTrace();
Log.e(TAG,"Error writing to UDP server, will abort connection: "+e.getMessage());
}
}
private void writeTCP(Connection connection) {
try {
writePendingData(connection);
} catch (NotYetConnectedException ex) {
Log.e(TAG,"failed to write to unconnected socket: " + ex.getMessage());
} catch (IOException e) {
Log.e(TAG,"Error writing to server: " + e);
//close connection with vpn client
byte[] rstData = TCPPacketFactory.INSTANCE.createRstData(
connection.getLastIpHeader(), connection.getLastTcpHeader(), 0);
writer.write(rstData);
//remove session
Log.e(TAG,"failed to write to remote socket, aborting connection");
connection.setAbortingConnection(true);
}
}
private void writePendingData(Connection connection) throws IOException {
if (!connection.hasDataToSend()) return;
AbstractSelectableChannel channel = connection.getChannel();
byte[] data = connection.getSendingData();
ByteBuffer buffer = ByteBuffer.allocate(data.length);
buffer.put(data);
buffer.flip();
while (buffer.hasRemaining()) {
int bytesWritten = channel instanceof SocketChannel
? ((SocketChannel) channel).write(buffer)
: ((DatagramChannel) channel).write(buffer);
if (bytesWritten == 0) {
break;
}
}
if (buffer.hasRemaining()) {
// The channel's own buffer is full, so we have to save this for later.
Log.i(TAG, buffer.remaining() + " bytes unwritten for " + channel);
// Put the remaining data from the buffer back into the session
connection.setSendingData(buffer.compact());
// Subscribe to WRITE events, so we know when this is ready to resume.
connection.subscribeKey(SelectionKey.OP_WRITE);
} else {
// All done, all good -> wait until the next TCP PSH / UDP packet
connection.setDataForSendingReady(false);
// We don't need to know about WRITE events any more, we've written all our data.
// This is safe from races with new data, due to the session lock in NIO.
connection.unsubscribeKey(SelectionKey.OP_WRITE);
}
}
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/socket/SocketNIODataService.java
================================================
package com.network.proxy.vpn.socket;
import android.util.Log;
import com.network.proxy.vpn.Connection;
import com.network.proxy.vpn.TagKt;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.AbstractSelectableChannel;
import java.util.Iterator;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* A service that single-threadedly processes the events around our session connections,
* entirely via non-blocking NIO.
*
* It uses a Selector that fires on outgoing socket events (connected, readable, writable),
* handles the resulting operations, and keeps those subscriptions up to date.
*/
public class SocketNIODataService implements Runnable {
private final String TAG = TagKt.getTAG(this);
private final ReentrantLock nioSelectionLock = new ReentrantLock();
private final ReentrantLock nioHandlingLock = new ReentrantLock();
private final Selector selector = Selector.open();
private final SocketChannelReader reader;
private final SocketChannelWriter writer;
private volatile boolean shutdown = false;
public SocketNIODataService(ClientPacketWriter clientPacketWriter) throws IOException {
reader = new SocketChannelReader(clientPacketWriter);
writer = new SocketChannelWriter(clientPacketWriter);
}
@Override
public void run() {
Log.d(TAG,"SocketNIODataService starting in background...");
runTask();
}
public void registerSession(Connection connection) throws ClosedChannelException {
AbstractSelectableChannel channel = connection.getChannel();
boolean isConnected = channel instanceof DatagramChannel
? ((DatagramChannel) channel).isConnected()
: ((SocketChannel) channel).isConnected();
// Log.i(TAG, "Registering new session: " + session);
Lock selectorLock = lockSelector(selector);
try {
SelectionKey selectionKey = channel.register(selector,
isConnected
? SelectionKey.OP_READ
: SelectionKey.OP_CONNECT
);
connection.setSelectionKey(selectionKey);
selectionKey.attach(connection);
// Log.d(TAG, "Registered selector successfully");
} finally {
selectorLock.unlock();
}
}
private Lock lockSelector(Selector selector) {
boolean gotSelectionLock = nioSelectionLock.tryLock();
if (gotSelectionLock) return nioSelectionLock;
nioHandlingLock.lock(); // Ensure the NIO thread can't do anything on wakeup
selector.wakeup();
nioSelectionLock.lock(); // Actually get the lock we want
nioHandlingLock.unlock(); // Release the handling lock, which we no longer care about
return nioSelectionLock;
}
/**
* If the selector is currently select()ing, wake it up (e.g. to register changes to
* interestOps). If it's not (and so it probably will select() very soon anyway) do nothing.
* This is designed to be run after changing readyOps, to ensure the new ops get monitored
* immediately (and fire immediately, if already ready). Without this, that blocks.
*/
public void refreshSelect(Connection connection) {
boolean gotLock = nioSelectionLock.tryLock();
if (!gotLock) {
connection.getSelectionKey().selector().wakeup();
} else {
nioSelectionLock.unlock();
}
}
/**
* Shut down the NIO thread
*/
public void shutdown(){
this.shutdown = true;
selector.wakeup();
}
private void runTask(){
Log.i(TAG, "NIO selector is running...");
while(!shutdown){
try {
nioSelectionLock.lockInterruptibly();
selector.select();
} catch (IOException e) {
Log.e(TAG,"Error in Selector.select(): " + e.getMessage());
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
Log.e(TAG, e.toString());
}
continue;
} catch (InterruptedException ex) {
Log.i(TAG, "Select() interrupted");
} finally {
if (nioSelectionLock.isHeldByCurrentThread()) {
nioSelectionLock.unlock();
}
}
if (shutdown) {
break;
}
// A lock here makes it possible to reliably grab the selection lock above
nioHandlingLock.lock();
try {
Iterator iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
Connection connection = ((Connection) key.attachment());
synchronized (connection) { // Sessions are locked during processing (no VPN data races)
try {
processSelectionKey(key);
} catch (IOException e) {
synchronized (key) {
key.cancel();
}
}
}
iterator.remove();
if (shutdown) {
break;
}
}
} finally {
nioHandlingLock.unlock();
}
}
Log.i(TAG, "NIO selector shutdown");
}
private void processSelectionKey(SelectionKey key) throws IOException {
if (!key.isValid()) {
Log.d(TAG,"Invalid SelectionKey");
return;
}
SelectableChannel channel = key.channel();
Connection connection = ((Connection) key.attachment());
if (connection == null) {
Log.w(TAG, "Key fired with no session attached");
return;
}
if (channel instanceof SocketChannel && !connection.isConnected() && key.isConnectable()) {
SocketChannel socketChannel = (SocketChannel) channel;
if (socketChannel.isConnectionPending()) {
boolean connected = socketChannel.finishConnect();
connection.setConnected(connected);
} else {
throw new IllegalStateException("TCP channels must either be connected or pending connection");
}
}
if (isConnected(channel)) {
processConnectedSelection(key, connection);
}
}
private boolean isConnected(SelectableChannel channel) {
if (channel instanceof DatagramChannel) {
return ((DatagramChannel) channel).isConnected();
} else if (channel instanceof SocketChannel) {
return ((SocketChannel) channel).isConnected();
} else {
throw new IllegalArgumentException("isConnected on unexpected channel type: " + channel);
}
}
private void processConnectedSelection(SelectionKey key, Connection connection) {
// Whilst connected, we always want READ and not CONNECT events
connection.unsubscribeKey(SelectionKey.OP_CONNECT);
connection.subscribeKey(SelectionKey.OP_READ);
processSelectorRead(key, connection);
processPendingWrite(key, connection);
}
private void processSelectorRead(SelectionKey selectionKey, Connection connection) {
boolean canRead;
synchronized (selectionKey) {
// There's a race here that requires a lock, as isReadable requires isValid
canRead = selectionKey.isValid() && selectionKey.isReadable();
}
if (canRead) reader.read(connection);
}
private void processPendingWrite(SelectionKey selectionKey, Connection connection) {
// Nothing to write? Skip this entirely, and make sure we're not subscribed
if (!connection.hasDataToSend() || !connection.isDataForSendingReady()) {
connection.unsubscribeKey(SelectionKey.OP_WRITE);
return;
}
boolean canWrite;
synchronized (selectionKey) {
// There's a race here that requires a lock, as isReadable requires isValid
canWrite = selectionKey.isValid() && selectionKey.isWritable();
}
if (canWrite) {
connection.unsubscribeKey(SelectionKey.OP_WRITE);
writer.write(connection); // This will resubscribe to OP_WRITE if it can't complete
}
}
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/Packet.kt
================================================
package com.network.proxy.vpn.transport
import com.network.proxy.vpn.transport.protocol.IP4Header
import com.network.proxy.vpn.transport.protocol.TransportHeader
class Packet(var ipHeader: IP4Header, var transportHeader: TransportHeader, var buffer: ByteArray) {
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/icmp/ICMPPacket.java
================================================
package com.network.proxy.vpn.transport.icmp;
import androidx.annotation.NonNull;
public class ICMPPacket {
// Two ICMP packets we can handle: simple ping & pong
public static final byte ECHO_REQUEST_TYPE = 8;
public static final byte ECHO_SUCCESS_TYPE = 0;
// One very common packet we ignore: connection rejection. Unclear why this happens,
// random incoming connections that the phone tries to reply to? Nothing we can do though,
// as we can't forward ICMP onwards, and we can't usefully respond or react.
public static final byte DESTINATION_UNREACHABLE_TYPE = 3;
public final byte type;
final byte code; // 0 for request, 0 for success, 0 - 15 for error subtypes
final int checksum;
final int identifier;
final int sequenceNumber;
final byte[] data;
ICMPPacket(
int type,
int code,
int checksum,
int identifier,
int sequenceNumber,
byte[] data
) {
this.type = (byte) type;
this.code = (byte) code;
this.checksum = checksum;
this.identifier = identifier;
this.sequenceNumber = sequenceNumber;
this.data = data;
}
@NonNull
public String toString() {
return "ICMP packet type " + type + "/" + code + " id:" + identifier +
" seq:" + sequenceNumber + " and " + data.length + " bytes of data";
}
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/icmp/ICMPPacketFactory.java
================================================
package com.network.proxy.vpn.transport.icmp;
import androidx.annotation.NonNull;
import com.network.proxy.vpn.transport.protocol.IP4Header;
import com.network.proxy.vpn.util.PacketUtil;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
public class ICMPPacketFactory {
public static ICMPPacket parseICMPPacket(@NonNull ByteBuffer stream) {
final byte type = stream.get();
final byte code = stream.get();
final int checksum = stream.getShort();
final int identifier = stream.getShort();
final int sequenceNumber = stream.getShort();
final byte[] data = new byte[stream.remaining()];
stream.get(data);
return new ICMPPacket(type, code, checksum, identifier, sequenceNumber, data);
}
public static ICMPPacket buildSuccessPacket(ICMPPacket requestPacket) {
return new ICMPPacket(
0,
0,
0,
requestPacket.identifier,
requestPacket.sequenceNumber,
requestPacket.data
);
}
public static byte[] packetToBuffer(IP4Header ipHeader, ICMPPacket packet) {
byte[] ipData = ipHeader.toBytes();
ByteArrayOutputStream icmpDataBuffer = new ByteArrayOutputStream();
icmpDataBuffer.write(packet.type);
icmpDataBuffer.write(packet.code);
icmpDataBuffer.write(asShortBytes(0 /* checksum placeholder */), 0, 2);
if (packet.type == ICMPPacket.ECHO_REQUEST_TYPE || packet.type == ICMPPacket.ECHO_SUCCESS_TYPE) {
icmpDataBuffer.write(asShortBytes(packet.identifier), 0, 2);
icmpDataBuffer.write(asShortBytes(packet.sequenceNumber), 0, 2);
byte[] extraData = packet.data;
icmpDataBuffer.write(extraData, 0, extraData.length);
} else {
throw new IllegalArgumentException("Can't serialize unrecognized ICMP packet type");
}
byte[] icmpPacketData = icmpDataBuffer.toByteArray();
byte[] checksum = PacketUtil.INSTANCE.calculateChecksum(icmpPacketData, 0, icmpPacketData.length);
ByteBuffer resultBuffer = ByteBuffer.allocate(ipData.length + icmpPacketData.length);
resultBuffer.put(ipData);
resultBuffer.put(icmpPacketData);
// Replace the checksum placeholder
resultBuffer.position(ipData.length + 2);
resultBuffer.put(checksum);
resultBuffer.position(0);
byte[] result = new byte[resultBuffer.remaining()];
resultBuffer.get(result);
return result;
}
private static byte[] asShortBytes(int value) {
return ByteBuffer.allocate(2).putShort((short) value).array();
}
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/IP4Header.kt
================================================
package com.network.proxy.vpn.transport.protocol
import android.util.Log
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* IPv4报头的数据结构。
*/
data class IP4Header(
var ipVersion: Byte = 0, //对于IPv4,其值为4(因此命名为IPv4)。 4bit
private var internetHeaderLength: Byte = 0, //头部长度 4bit
private var diffTypeOfService: Byte, //差分服务代码点 =>6位
private var ecn: Byte = 0, //显式拥塞通知(ECN)
var totalLength: Int = 0, //此IP数据包的总长度 16bit
var identification: Int = 0, //主要用于唯一标识单个IP数据报的片段组。 16bit
private var mayFragment: Boolean, // 1bit 用于指示数据报是否可以分段。
private var lastFragment: Boolean, // 1bit 用于指示数据报是否是片段中的最后一个。
var fragmentOffset: Short = 0, //13bit,指定特定片段相对于原始未分段的IP数据报的开始的偏移量。
private var timeToLive: Byte = 0, //用于防止数据报持续存在。8bit
var protocol: Byte = 0, //定义IP数据报的数据部分中使用的协议。 8bit
var headerChecksum: Int = 0, //用于对头部进行错误检查的16位字段。 16bit
var sourceIP: Int = 0, //发送者的IPv4地址。 32bit
var destinationIP: Int = 0 //接收者的IPv4地址。 32bit
) {
//用于控制或识别片段的3比特字段。
//bit 0: 保留;必须为零
//bit 1: Don't Fragment (DF)
//bit 2: More Fragments (MF)
private var flag: Byte = initFlag()
private fun initFlag(): Byte {
var initFlag = 0
if (mayFragment) {
initFlag = 0x40
}
if (lastFragment) {
initFlag = (initFlag or 0x20)
}
return initFlag.toByte()
}
fun setMayFragment(mayFragment: Boolean) {
this.mayFragment = mayFragment
flag = if (mayFragment) {
(flag.toInt() or 0x40).toByte()
} else {
(flag.toInt() and 0xBF).toByte()
}
}
fun getIPHeaderLength(): Int {
return internetHeaderLength * 4
}
fun copy(): IP4Header {
return IP4Header(
ipVersion, internetHeaderLength, diffTypeOfService, ecn, totalLength, identification,
mayFragment, lastFragment, fragmentOffset, timeToLive, protocol, headerChecksum,
sourceIP, destinationIP
)
}
fun toBytes(): ByteArray {
val buffer = ByteBuffer.allocate(getIPHeaderLength())
buffer.order(ByteOrder.BIG_ENDIAN)
val versionAndHeaderLength = (ipVersion.toInt() shl 4) + internetHeaderLength
buffer.put(versionAndHeaderLength.toByte())
val typeOfService: Byte = (diffTypeOfService.toInt() shl 2 and (ecn
.toInt() and 0xFF)).toByte()
buffer.put(typeOfService)
buffer.putShort(totalLength.toShort())
buffer.putShort(identification.toShort())
//组合标志和部分片段偏移
buffer.put((fragmentOffset.toInt() shr 8 and 0x1F or flag.toInt()).toByte())
buffer.put(fragmentOffset.toByte())
buffer.put(timeToLive)
buffer.put(protocol)
buffer.putShort(headerChecksum.toShort())
buffer.putInt(sourceIP)
buffer.putInt(destinationIP)
return buffer.array()
}
}
object IPPacketFactory {
private const val IP4_HEADER_SIZE = 20
private const val IP4_VERSION = 0x04
/**
* 从给定的ByteBuffer流创建IPv4标头
*/
fun createIP4Header(buffer: ByteBuffer): IP4Header? {
if (buffer.remaining() < IP4_HEADER_SIZE) {
throw IllegalArgumentException("IP header byte array must have at least $IP4_HEADER_SIZE bytes")
}
val versionAndHeaderLength: Byte = buffer.get()
val ipVersion = (versionAndHeaderLength.toInt() shr 4).toByte()
if (ipVersion.toInt() != IP4_VERSION) {
Log.e("IPPacketFactory", "Invalid IP version $ipVersion")
return null
}
val internetHeaderLength = (versionAndHeaderLength.toInt() and 0x0F).toByte()
val typeOfService = buffer.get().toInt()
val diffTypeOfService: Byte = (typeOfService shr 2).toByte();
val ecn: Byte = (typeOfService and 0x03).toByte()
val totalLength: Int = buffer.getShort().toInt()
val identification: Int = buffer.getShort().toInt()
val flagsAndFragmentOffset: Short = buffer.getShort()
val mayFragment = flagsAndFragmentOffset.toInt() and 0x4000 != 0
val lastFragment = flagsAndFragmentOffset.toInt() and 0x2000 != 0
val fragmentOffset = (flagsAndFragmentOffset.toInt() and 0x1FFF).toShort()
val timeToLive: Byte = buffer.get()
val protocol: Byte = buffer.get()
val checksum: Int = buffer.getShort().toInt()
val sourceIp: Int = buffer.getInt()
val desIp: Int = buffer.getInt()
if (internetHeaderLength > 5) {
// drop the IP option
for (i in 0 until (internetHeaderLength - 5)) {
buffer.getInt()
}
}
return IP4Header(
ipVersion, internetHeaderLength, diffTypeOfService, ecn, totalLength, identification,
mayFragment, lastFragment, fragmentOffset, timeToLive, protocol, checksum,
sourceIp, desIp
)
}
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/TCPHeader.kt
================================================
package com.network.proxy.vpn.transport.protocol
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* TCP报头的数据结构。
*/
class TCPHeader(
private var sourcePort: Int = 0, //源端口号 16bit
private var destinationPort: Int = 0, //目的端口号 16bit
var sequenceNumber: Long = 0, //序列号 32bit
var ackNumber: Long = 0, //确认号 32bit
var dataOffset: Int = 0, //数据偏移4bit
var isNS: Boolean = false, //ECN-nonce concealment protection (experimental: see RFC 3540)
var flags: Int = 0, //标志位 9bit
var windowSize: Int = 0, //窗口大小 16bit
var checksum: Int = 0, //校验和 16bit
private var urgentPointer: Int = 0, //紧急指针 16bit
var options: ByteArray? = null //选项
) : TransportHeader {
//options
var maxSegmentSize: Short = 0
private var windowScale: Byte = 0
private var isSelectiveAckPermitted = false
var timeStampSender = 0
var timeStampReplyTo = 0
companion object {
private const val END_OF_OPTIONS_LIST: Byte = 0
private const val NO_OPERATION: Byte = 1
private const val MAX_SEGMENT_SIZE: Byte = 2
private const val WINDOW_SCALE: Byte = 3
private const val SELECTIVE_ACK_PERMITTED: Byte = 4
private const val TIME_STAMP: Byte = 8
}
fun isSYN(): Boolean {
return flags and 0x02 != 0
}
fun isFIN(): Boolean {
return flags and 0x01 != 0
}
fun isRST(): Boolean {
return flags and 0x04 != 0
}
fun isPSH(): Boolean {
return flags and 0x08 != 0
}
fun isACK(): Boolean {
return flags and 0x10 != 0
}
fun isURG(): Boolean {
return flags and 0x20 != 0
}
fun isECE(): Boolean {
return flags and 0x40 != 0
}
fun isCWR(): Boolean {
return flags and 0x80 != 0
}
fun setIsRST(isRST: Boolean) {
flags = if (isRST) {
(flags or 0x04)
} else {
(flags and 0xFB)
}
}
fun setIsSYN(isSYN: Boolean) {
flags = if (isSYN) {
(flags or 0x02)
} else {
(flags and 0xFD)
}
}
fun setIsFIN(isFIN: Boolean) {
flags = if (isFIN) {
(flags or 0x01)
} else {
(flags and 0xFE)
}
}
fun setIsPSH(isPSH: Boolean) {
flags = if (isPSH) {
(flags or 0x08)
} else {
(flags and 0xF7)
}
}
fun setIsACK(isACK: Boolean) {
flags = if (isACK) {
(flags or 0x10)
} else {
(flags and 0xEF)
}
}
fun getTCPHeaderLength(): Int {
return dataOffset * 4
}
fun toBytes(): ByteArray {
val tcpHeaderLength = getTCPHeaderLength()
val tcpHeader = ByteArray(tcpHeaderLength)
val byteBuffer = ByteBuffer.wrap(tcpHeader)
byteBuffer.order(ByteOrder.BIG_ENDIAN)
byteBuffer.putShort(sourcePort.toShort())
byteBuffer.putShort(destinationPort.toShort())
byteBuffer.putInt(sequenceNumber.toInt())
byteBuffer.putInt(ackNumber.toInt())
//is ns and data offset
byteBuffer.put(((dataOffset shl 4) and 0xF0 or (if (isNS) 0x1 else 0x0)).toByte())
byteBuffer.put(flags.toByte())
byteBuffer.putShort(windowSize.toShort())
byteBuffer.putShort(checksum.toShort())
byteBuffer.putShort(urgentPointer.toShort())
// encodeTcpOptions()?.let {
// byteBuffer.put(it)
// }
return tcpHeader
}
fun copy(): TCPHeader {
return TCPHeader(
sourcePort, destinationPort, sequenceNumber, ackNumber,
dataOffset, isNS, flags, windowSize, checksum, urgentPointer,
options
)
}
private fun handleTcpOptions() {
if (options == null) {
return
}
var index = 0
val packet = ByteBuffer.wrap(options!!)
val optionsSize = options!!.size
while (index < optionsSize) {
val optionKind = packet.get()
index++
if (optionKind == END_OF_OPTIONS_LIST || optionKind == NO_OPERATION) {
continue
}
val size = packet.get()
index++
when (optionKind) {
MAX_SEGMENT_SIZE -> {
maxSegmentSize = packet.getShort()
index += 2
}
WINDOW_SCALE -> {
windowScale = packet.get()
index++
}
SELECTIVE_ACK_PERMITTED -> isSelectiveAckPermitted = true
TIME_STAMP -> {
timeStampSender = packet.getInt()
timeStampReplyTo = packet.getInt()
index += 8
}
else -> {
skipRemainingOptions(packet, size.toInt())
index = index + size - 2
}
}
}
}
private fun skipRemainingOptions(packet: ByteBuffer, size: Int) {
for (i in 2 until size) {
packet.get()
}
}
override fun getSourcePort(): Int {
return sourcePort
}
override fun getDestinationPort(): Int {
return destinationPort
}
fun setSourcePort(sourcePort: Int) {
this.sourcePort = sourcePort
}
fun setDestinationPort(destinationPort: Int) {
this.destinationPort = destinationPort
}
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/TCPPacketFactory.kt
================================================
package com.network.proxy.vpn.transport.protocol
import com.network.proxy.vpn.transport.Packet
import com.network.proxy.vpn.util.PacketUtil
import java.nio.ByteBuffer
import java.util.concurrent.ThreadLocalRandom
object TCPPacketFactory {
private const val TCP_HEADER_LENGTH = 20
/**
* 从tcp报文创建tcpHeader
*/
@JvmStatic
fun createTCPHeader(byteBuffer: ByteBuffer): TCPHeader {
if (byteBuffer.remaining() < TCP_HEADER_LENGTH) {
throw IllegalArgumentException("Invalid TCP Header Length")
}
val sourcePort: Int = byteBuffer.getShort().toInt() and 0xFFFF
val destinationPort: Int = byteBuffer.getShort().toInt() and 0xFFFF
val sequenceNumber: Long = byteBuffer.getInt().toLong()
val ackNumber: Long = byteBuffer.getInt().toLong()
val dataOffsetAndReserved = byteBuffer.get()
val dataOffset = (dataOffsetAndReserved.toInt() and 0xF0) shr 4
val isNs: Boolean = dataOffsetAndReserved.toInt() and 0x1 > 0x0
val flags = byteBuffer.get().toInt()
val window = byteBuffer.short.toInt()
val checksum = byteBuffer.short.toInt()
val urgentPointer = byteBuffer.short.toInt()
var optionsAndPadding: ByteArray? = null
val optionsSize = dataOffset - 5
if (optionsSize > 0) {
optionsAndPadding = ByteArray(optionsSize * 4)
byteBuffer.get(optionsAndPadding, 0, optionsSize * 4)
}
return TCPHeader(
sourcePort, destinationPort, sequenceNumber, ackNumber,
dataOffset, isNs, flags, window, checksum, urgentPointer, optionsAndPadding
)
}
/**
* 创建带有RST标志的数据包,以便在需要重置时发送到客户端。
*/
fun createRstData(ipHeader: IP4Header, tcpHeader: TCPHeader, dataLength: Int): ByteArray {
val ip = ipHeader.copy()
val tcp = tcpHeader.copy()
var ackNumber: Long = 0
var seqNumber: Long = 0
if (tcp.ackNumber > 0) {
seqNumber = tcp.ackNumber
} else {
ackNumber = tcp.sequenceNumber + dataLength
}
tcp.ackNumber = ackNumber
tcp.sequenceNumber = seqNumber
//将IP从源翻转到目标
flipIp(ip, tcp)
ip.identification = 0
tcp.flags = 0
tcp.isNS = false
tcp.setIsRST(true)
tcp.dataOffset = 5
tcp.options = null
tcp.windowSize = 0
//重新计算IP长度
val totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()
ip.totalLength = totalLength
return createPacketData(ip, tcp, null)
}
/**
* 创建数据包数据以发送回客户端
*/
@JvmStatic
fun createResponsePacketData(
ipHeader: IP4Header, tcpHeader: TCPHeader, packetData: ByteArray?, isPsh: Boolean,
ackNumber: Long, seqNumber: Long, timeSender: Int, timeReplyTo: Int
): ByteArray {
val ip = ipHeader.copy()
val tcp = tcpHeader.copy()
flipIp(ip, tcp)
tcp.ackNumber = ackNumber
tcp.sequenceNumber = seqNumber
ip.identification = PacketUtil.getPacketId()
//总是发送ACK
//ACK is always sent
tcp.setIsACK(true)
tcp.setIsSYN(false)
tcp.setIsPSH(isPsh)
tcp.setIsFIN(false)
tcp.timeStampSender = timeSender
tcp.timeStampReplyTo = timeReplyTo
tcp.dataOffset = 5
tcp.options = null
var totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()
if (packetData != null) {
totalLength += packetData.size
}
ip.totalLength = totalLength
return createPacketData(ip, tcp, packetData)
}
/**
* 向客户端确认服务器已收到请求。
*/
@JvmStatic
fun createResponseAckData(
ipHeader: IP4Header, tcpHeader: TCPHeader, ackToClient: Long
): ByteArray {
val ip = ipHeader.copy()
val tcp = tcpHeader.copy()
flipIp(ip, tcp)
val seqNumber = tcp.ackNumber
tcp.ackNumber = ackToClient
tcp.sequenceNumber = seqNumber
ip.identification = PacketUtil.getPacketId()
//ACK
tcp.setIsACK(true)
tcp.setIsSYN(false)
tcp.setIsPSH(false)
tcp.setIsFIN(false)
tcp.dataOffset = 5
tcp.options = null
ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()
return createPacketData(ip, tcp, null)
}
//将IP从源翻转到目标
private fun flipIp(ip: IP4Header, tcp: TCPHeader) {
val sourceIp = ip.destinationIP
val destIp = ip.sourceIP
val sourcePort = tcp.getDestinationPort()
val destPort = tcp.getSourcePort()
ip.destinationIP = destIp
ip.sourceIP = sourceIp
tcp.setDestinationPort(destPort)
tcp.setSourcePort(sourcePort)
}
/**
* 通过写回客户端流创建SYN-ACK数据包数据
*/
fun createSynAckPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader): Packet {
val ip = ipHeader.copy()
val tcp = tcpHeader.copy()
flipIp(ip, tcp)
//ack = received sequence + 1
val ackNumber = tcpHeader.sequenceNumber + 1
tcp.ackNumber = ackNumber
//服务器生成的初始序列号
val seqNumber = ThreadLocalRandom.current().nextLong(0, 100000)
tcp.sequenceNumber = seqNumber
//SYN-ACK
tcp.setIsACK(true)
tcp.setIsSYN(true)
tcp.timeStampReplyTo = tcp.timeStampSender
tcp.timeStampSender = PacketUtil.currentTime
tcp.dataOffset = 5
tcp.options = null
ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()
return Packet(ip, tcp, createPacketData(ip, tcp, null))
}
/**
* 创建发送到客户端的FIN-ACK
*/
fun createFinAckData(
ipHeader: IP4Header, tcpHeader: TCPHeader, ackToClient: Long,
seqToClient: Long, isFin: Boolean, isAck: Boolean
): ByteArray {
val ip = ipHeader.copy()
val tcp = tcpHeader.copy()
flipIp(ip, tcp)
tcp.ackNumber = ackToClient
tcp.sequenceNumber = seqToClient
ip.identification = PacketUtil.getPacketId()
//ACK
tcp.setIsACK(isAck)
tcp.setIsSYN(false)
tcp.setIsPSH(false)
tcp.setIsFIN(isFin)
tcp.dataOffset = 5
tcp.options = null
ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()
return createPacketData(ip, tcp, null)
}
fun createFinData(
ip: IP4Header, tcp: TCPHeader, ackNumber: Long, seqNumber: Long,
timeSender: Int, timeReplyTo: Int
): ByteArray {
//将IP从源翻转到目标
flipIp(ip, tcp)
tcp.ackNumber = ackNumber
tcp.sequenceNumber = seqNumber
ip.identification = PacketUtil.getPacketId()
tcp.timeStampReplyTo = timeReplyTo
tcp.timeStampSender = timeSender
tcp.flags = 0
tcp.isNS = false
tcp.setIsACK(true)
tcp.setIsFIN(true)
tcp.dataOffset = 5
tcp.options = null
//窗口大小应为零
tcp.windowSize = 0
ip.totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()
return createPacketData(ip, tcp, null)
}
/**
* 从tcpHeader创建tcp报文
*/
private fun createPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader, data: ByteArray?):
ByteArray {
val dataLength = data?.size ?: 0
val buffer =
ByteBuffer.allocate(ipHeader.getIPHeaderLength() + tcpHeader.getTCPHeaderLength() + dataLength)
val ipBuffer = ipHeader.toBytes()
val tcpBuffer = tcpHeader.toBytes()
buffer.put(ipBuffer)
buffer.put(tcpBuffer)
data?.let { buffer.put(it) }
val zero = byteArrayOf(0, 0)
//计算前先将校验和清零
buffer.position(10)
buffer.put(zero)
val ipChecksum = PacketUtil.calculateChecksum(buffer.array(), 0, ipBuffer.size)
buffer.position(10)
buffer.put(ipChecksum)
val tcpStart = ipBuffer.size
buffer.position(tcpStart + 16)
buffer.put(zero)
val tcpChecksum = PacketUtil.calculateTCPHeaderChecksum(
buffer.array(), tcpStart, tcpBuffer.size + dataLength,
ipHeader.destinationIP, ipHeader.sourceIP
)
//将新的校验和写回阵列
buffer.position(tcpStart + 16)
buffer.put(tcpChecksum)
return buffer.array()
}
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/TransportHeader.kt
================================================
package com.network.proxy.vpn.transport.protocol
interface TransportHeader {
fun getSourcePort(): Int
fun getDestinationPort(): Int
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/transport/protocol/UDPHeader.kt
================================================
package com.network.proxy.vpn.transport.protocol
import com.network.proxy.vpn.util.PacketUtil
import java.nio.ByteBuffer
/**
* UDP报头的数据结构。
*/
data class UDPHeader(
var sourcePort: Int = 0, //源端口号 16bit
var destinationPort: Int = 0, //目的端口号 16bit
var length: Int = 0, //UDP数据报长度 16bit
var checksum: Int = 0 //校验和 16bit
)
object UDPPacketFactory {
@JvmStatic
fun createUDPHeader(stream: ByteBuffer): UDPHeader {
require(stream.remaining() >= 8) { "Minimum UDP header is 8 bytes." }
val srcPort = stream.getShort().toInt() and 0xffff
val destPort = stream.getShort().toInt() and 0xffff
val length = stream.getShort().toInt() and 0xffff
val checksum = stream.getShort().toInt()
return UDPHeader(srcPort, destPort, length, checksum)
}
/**
* 创建用于响应vpn客户端的数据包
*/
@JvmStatic
fun createResponsePacket(ip: IP4Header, udp: UDPHeader, packetData: ByteArray?): ByteArray {
val buffer: ByteArray
var udpLen = 8
if (packetData != null) {
udpLen += packetData.size
}
val srcPort = udp.destinationPort
val destPort = udp.sourcePort
val ipHeader = ip.copy()
val srcIp = ip.destinationIP
val destIp = ip.sourceIP
ipHeader.setMayFragment(false)
ipHeader.sourceIP = srcIp
ipHeader.destinationIP = destIp
ipHeader.identification = PacketUtil.getPacketId()
//ip的长度是整个数据包的长度 => IP header length + UDP header length (8) + UDP body length
val totalLength = ipHeader.getIPHeaderLength() + udpLen
ipHeader.totalLength = totalLength
buffer = ByteArray(totalLength)
val ipData = ipHeader.toBytes()
// clear IP checksum
ipData[11] = 0
ipData[10] = 0
//calculate checksum for IP header
val ipChecksum = PacketUtil.calculateChecksum(ipData, 0, ipData.size)
//write result of checksum back to buffer
System.arraycopy(ipChecksum, 0, ipData, 10, 2)
System.arraycopy(ipData, 0, buffer, 0, ipData.size)
//copy UDP header to buffer
var start = ipData.size
val intContainer = ByteArray(4)
PacketUtil.writeIntToBytes(srcPort, intContainer, 0)
//extract the last two bytes of int value
System.arraycopy(intContainer, 2, buffer, start, 2)
start += 2
PacketUtil.writeIntToBytes(destPort, intContainer, 0)
System.arraycopy(intContainer, 2, buffer, start, 2)
start += 2
PacketUtil.writeIntToBytes(udpLen, intContainer, 0)
System.arraycopy(intContainer, 2, buffer, start, 2)
start += 2
val checksum: Short = 0
PacketUtil.writeIntToBytes(checksum.toInt(), intContainer, 0)
System.arraycopy(intContainer, 2, buffer, start, 2)
start += 2
//now copy udp data
if (packetData != null) System.arraycopy(packetData, 0, buffer, start, packetData.size)
return buffer
}
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/util/PacketUtil.kt
================================================
package com.network.proxy.vpn.util
import android.util.Log
import com.network.proxy.vpn.formatTag
import com.network.proxy.vpn.transport.protocol.IP4Header
import com.network.proxy.vpn.transport.protocol.TCPHeader
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* Helper class to perform various useful task
*/
object PacketUtil {
@get:Synchronized
private var packetId = 0
fun getPacketId() = packetId++
val currentTime: Int
get() = (System.currentTimeMillis() / 1000).toInt()
/**
* convert int to byte array
* [...](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html)
*
* @param value int value 32 bits
* @param buffer array of byte to write to
* @param offset position to write to
*/
fun writeIntToBytes(value: Int, buffer: ByteArray, offset: Int) {
if (buffer.size - offset < 4) {
return
}
buffer[offset] = (value ushr 24 and 0x000000FF).toByte()
buffer[offset + 1] = (value shr 16 and 0x000000FF).toByte()
buffer[offset + 2] = (value shr 8 and 0x000000FF).toByte()
buffer[offset + 3] = (value and 0x000000FF).toByte()
}
/**
* convert array of max 4 bytes to int
*
* @param buffer byte array
* @param start Starting point to be read in byte array
* @param length Length to be read
* @return value of int
*/
fun getNetworkInt(buffer: ByteArray, start: Int, length: Int): Int {
var value = 0
var end = start + Math.min(length, 4)
if (end > buffer.size) end = buffer.size
for (i in start until end) {
value = value or (buffer[i].toInt() and 0xFF)
if (i < end - 1) value = value shl 8
}
return value
}
/**
* validate TCP header checksum
*
* @param source Source Port
* @param destination Destination Port
* @param data Payload
* @param tcpLength TCP Header length
* @return boolean
*/
fun isValidTCPChecksum(
source: Int, destination: Int,
data: ByteArray, tcpLength: Short, tcpOffset: Int
): Boolean {
var buffersize = tcpLength + 12
var isodd = false
if (buffersize % 2 != 0) {
buffersize++
isodd = true
}
val buffer = ByteBuffer.allocate(buffersize)
buffer.putInt(source)
buffer.putInt(destination)
buffer.put(0.toByte()) //reserved => 0
buffer.put(6.toByte()) //TCP protocol => 6
buffer.putShort(tcpLength)
buffer.put(data, tcpOffset, tcpLength.toInt())
if (isodd) {
buffer.put(0.toByte())
}
return isValidIPChecksum(buffer.array(), buffersize)
}
/**
* validate IP Header checksum
*
* @param data byte stream
* @return boolean
*/
private fun isValidIPChecksum(data: ByteArray, length: Int): Boolean {
var start = 0
var sum = 0
while (start < length) {
sum += getNetworkInt(data, start, 2)
start = start + 2
}
//carry over one's complement
while (sum shr 16 > 0) sum = (sum and 0xffff) + (sum shr 16)
//flip the bit to get one' complement
sum = sum.inv()
val buffer = ByteBuffer.allocate(4)
buffer.putInt(sum)
return buffer.getShort(2).toInt() == 0
}
fun calculateChecksum(data: ByteArray, offset: Int, length: Int): ByteArray {
var start = offset
var sum = 0
while (start < length) {
sum += getNetworkInt(data, start, 2)
start = start + 2
}
//carry over one's complement
while (sum shr 16 > 0) {
sum = (sum and 0xffff) + (sum shr 16)
}
//flip the bit to get one' complement
sum = sum.inv()
//extract the last two byte of int
val checksum = ByteArray(2)
checksum[0] = (sum shr 8).toByte()
checksum[1] = sum.toByte()
return checksum
}
fun calculateTCPHeaderChecksum(
data: ByteArray,
offset: Int,
tcplength: Int,
destip: Int,
sourceip: Int
): ByteArray {
var buffersize = tcplength + 12
var odd = false
if (buffersize % 2 != 0) {
buffersize++
odd = true
}
val buffer = ByteBuffer.allocate(buffersize)
buffer.order(ByteOrder.BIG_ENDIAN)
//create virtual header
buffer.putInt(sourceip)
buffer.putInt(destip)
buffer.put(0.toByte()) //reserved => 0
buffer.put(6.toByte()) //tcp protocol => 6
buffer.putShort(tcplength.toShort())
//add actual header + data
buffer.put(data, offset, tcplength)
//padding last byte to zero
if (odd) {
buffer.put(0.toByte())
}
val tcparray = buffer.array()
return calculateChecksum(tcparray, 0, buffersize)
}
fun intToIPAddress(addressInt: Int): String {
return (addressInt ushr 24 and 0x000000FF).toString() + "." +
(addressInt ushr 16 and 0x000000FF) + "." +
(addressInt ushr 8 and 0x000000FF) + "." +
(addressInt and 0x000000FF)
}
fun getOutput(
ipHeader: IP4Header, tcpheader: TCPHeader,
packetData: ByteArray
): String {
val tcpLength = (packetData.size -
ipHeader.getIPHeaderLength()).toShort()
val isValidChecksum = isValidTCPChecksum(
ipHeader.sourceIP, ipHeader.destinationIP,
packetData, tcpLength, ipHeader.getIPHeaderLength()
)
val isValidIPChecksum = isValidIPChecksum(
packetData,
ipHeader.getIPHeaderLength()
)
val packetBodyLength = (packetData.size - ipHeader.getIPHeaderLength()
- tcpheader.getTCPHeaderLength())
val str = StringBuilder("\r\nIP Version: ")
.append(ipHeader.ipVersion.toInt())
.append("\r\nProtocol: ").append(ipHeader.protocol.toInt())
.append("\r\nID# ").append(ipHeader.identification)
.append("\r\nTotal Length: ").append(ipHeader.totalLength)
.append("\r\nData Length: ").append(packetBodyLength)
.append("\r\nDest: ").append(intToIPAddress(ipHeader.destinationIP))
.append(":").append(tcpheader.getDestinationPort())
.append("\r\nSrc: ").append(intToIPAddress(ipHeader.sourceIP))
.append(":").append(tcpheader.getSourcePort())
.append("\r\nACK: ").append(tcpheader.ackNumber)
.append("\r\nSeq: ").append(tcpheader.sequenceNumber)
.append("\r\nIP Header length: ").append(ipHeader.getIPHeaderLength())
.append("\r\nTCP Header length: ").append(tcpheader.getTCPHeaderLength())
.append("\r\nACK: ").append(tcpheader.isACK())
.append("\r\nSYN: ").append(tcpheader.isSYN())
.append("\r\nCWR: ").append(tcpheader.isCWR())
.append("\r\nECE: ").append(tcpheader.isECE())
.append("\r\nFIN: ").append(tcpheader.isFIN())
.append("\r\nNS: ").append(tcpheader.isNS)
.append("\r\nPSH: ").append(tcpheader.isPSH())
.append("\r\nRST: ").append(tcpheader.isRST())
.append("\r\nURG: ").append(tcpheader.isURG())
.append("\r\nIP checksum: ").append(ipHeader.headerChecksum)
.append("\r\nIs Valid IP Checksum: ").append(isValidIPChecksum)
.append("\r\nTCP Checksum: ").append(tcpheader.checksum)
.append("\r\nIs Valid TCP checksum: ").append(isValidChecksum)
.append("\r\nFragment Offset: ").append(ipHeader.fragmentOffset.toInt())
.append("\r\nWindow: ").append(tcpheader.windowSize)
.append("\r\nData Offset: ").append(tcpheader.dataOffset)
return str.toString()
}
/**
* detect packet corruption flag in tcp options sent from client ACK
*
* @param tcpHeader TCPHeader
* @return boolean
*/
fun isPacketCorrupted(tcpHeader: TCPHeader): Boolean {
val options = tcpHeader.options
if (options != null) {
var i = 0
while (i < options.size) {
val kind = options[i]
if (kind.toInt() == 0 || kind.toInt() == 1) {
} else if (kind.toInt() == 2) {
i += 3
} else if (kind.toInt() == 3 || kind.toInt() == 14) {
i += 2
} else if (kind.toInt() == 4) {
i++
} else if (kind.toInt() == 5 || kind.toInt() == 15) {
i = i + options[++i] - 2
} else if (kind.toInt() == 8) {
i += 9
} else if (kind.toInt() == 23) {
return true
} else {
Log.e(
formatTag(PacketUtil::class.java.name),
"unknown option: $kind"
)
}
i++
}
}
return false
}
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/util/ProcessInfoManager.kt
================================================
package com.network.proxy.vpn.util
import android.content.Context
import android.net.ConnectivityManager
import android.os.Build
import android.os.Process
import android.system.OsConstants
import android.util.Log
import androidx.annotation.RequiresApi
import com.network.proxy.ProxyVpnService
import com.network.proxy.plugin.ProcessInfo
import com.network.proxy.vpn.Connection
import kotlinx.coroutines.CoroutineScope
import java.io.File
import java.net.InetSocketAddress
import java.nio.channels.SocketChannel
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* 进程信息管理器,用于获取进程信息
* @author wanghongen
*/
class ProcessInfoManager private constructor() {
companion object {
@Suppress("all")
val instance = ProcessInfoManager()
}
class NetworkInfo(val uid: Int, val remoteHost: String, val remotePort: Int)
private val localPortCache =
SimpleCache(10_000, 60, TimeUnit.SECONDS)
private val appInfoCache = SimpleCache(10_000, 300, TimeUnit.SECONDS)
var activity: Context? = null
@RequiresApi(Build.VERSION_CODES.N)
fun setConnectionOwnerUid(connection: Connection) {
CoroutineScope(Dispatchers.IO).launch {
val sourceAddress =
InetSocketAddress(PacketUtil.intToIPAddress(connection.sourceIp), connection.sourcePort)
val destinationAddress = InetSocketAddress(
PacketUtil.intToIPAddress(connection.destinationIp), connection.destinationPort
)
val uid = getProcessInfoUid(sourceAddress, destinationAddress)
val channel = connection.channel
if (uid != null && uid != Process.INVALID_UID && channel is SocketChannel && channel.isOpen) {
try {
val localAddress = channel.localAddress as InetSocketAddress
val networkInfo =
NetworkInfo(uid, destinationAddress.hostString, destinationAddress.port)
localPortCache.put(localAddress.port, networkInfo)
} catch (e: java.nio.channels.ClosedChannelException) {
Log.w("ProcessInfoManager", "setConnectionOwnerUid", e)
}
}
}
}
fun removeConnection(connection: Connection) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return
}
val channel = connection.channel
if (channel is SocketChannel && channel.isOpen) {
try {
val localAddress = channel.localAddress as InetSocketAddress
localPortCache.remove(localAddress.port)
} catch (e: java.nio.channels.ClosedChannelException) {
Log.w("ProcessInfoManager", "removeConnection", e)
}
}
}
@RequiresApi(Build.VERSION_CODES.N)
private fun getProcessInfoUid(
localAddress: InetSocketAddress, remoteAddress: InetSocketAddress
): Int? {
// Log.d(TAG, "getProcessInfo: $localAddress $remoteAddress")
if (activity == null) {
return null
}
try {
val connectivityManager: ConnectivityManager =
activity!!.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val uid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
connectivityManager.getConnectionOwnerUid(
OsConstants.IPPROTO_TCP, localAddress, remoteAddress
)
} else {
val method = ConnectivityManager::class.java.getMethod(
"getConnectionOwnerUid",
Int::class.javaPrimitiveType,
InetSocketAddress::class.java,
InetSocketAddress::class.java
)
method.invoke(
connectivityManager, OsConstants.IPPROTO_TCP, localAddress, remoteAddress
) as Int
}
if (uid != Process.INVALID_UID) {
return uid
}
} catch (e: Exception) {
Log.w("ProcessInfoManager", "Exception in getProcessInfoUid", e)
return null
}
Log.w(
"ProcessInfoManager",
"Failed to get UID for local address $localAddress and remote address $remoteAddress"
)
return null
}
suspend fun getProcessInfoByPort(host: String?, localPort: Int): ProcessInfo? {
val networkInfo = localPortCache.get(localPort)
if (networkInfo != null) {
val processInfo = getProcessInfo(networkInfo.uid)
if (processInfo != null) {
val result = processInfo.copy()
result["remoteHost"] = networkInfo.remoteHost
result["remotePort"] = networkInfo.remotePort
return result
}
return null
}
if (host == null || localPort <= 0 || ProxyVpnService.host == null || ProxyVpnService.port <= 0) {
Log.w("ProcessInfoManager", "Invalid host or local port: $host:$localPort or ProxyVpnService not initialized")
return null
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return withContext(Dispatchers.IO) {
val localAddress = InetSocketAddress(host, localPort)
val remoteAddress = InetSocketAddress(ProxyVpnService.host, ProxyVpnService.port)
val uid = getProcessInfoUid(localAddress, remoteAddress)
if (uid == null || uid == Process.INVALID_UID) {
return@withContext null
}
val processInfo = getProcessInfo(uid)
if (processInfo != null) {
localPortCache.put(
localPort, NetworkInfo(uid, remoteAddress.hostString, remoteAddress.port)
)
val result = processInfo.copy()
result["remoteHost"] = remoteAddress.hostString
result["remotePort"] = remoteAddress.port
return@withContext result
} else {
Log.w("ProcessInfoManager", "No process info found for UID: $uid")
null
}
}
} else {
Log.w("ProcessInfoManager", "Access to /proc/net/tcp is restricted on non-rooted devices.")
}
return null
}
fun getRemoteAddressByPort(localPort: Int): Map? {
val networkInfo = localPortCache.get(localPort)
if (networkInfo != null) {
return mapOf(
"remoteHost" to networkInfo.remoteHost,
"remotePort" to networkInfo.remotePort
)
}
return null
}
private fun getProcessInfo(uid: Int): ProcessInfo? {
var appInfo = appInfoCache.get(uid)
if (appInfo != null) return appInfo
val packageManager = activity?.packageManager ?: return null
val pkgNames: Array? = try {
packageManager.getPackagesForUid(uid)
} catch (e: Exception) {
Log.w("ProcessInfoManager", "getPackagesForUid SecurityException: $uid", e)
null
}
if (pkgNames == null) return null
for (pkgName in pkgNames) {
try {
val applicationInfo = packageManager.getApplicationInfo(pkgName, 0)
appInfo = ProcessInfo.create(packageManager, applicationInfo)
appInfoCache.put(uid, appInfo)
return appInfo
} catch (e: Exception) {
// Ignore packages that can't be found
}
}
return null
}
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/util/SimpleCache.kt
================================================
package com.network.proxy.vpn.util
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class SimpleCache(
private val maxSize: Int,
private val expireAfterAccess: Long,
private val timeUnit: TimeUnit
) {
private val cache = ConcurrentHashMap>()
companion object {
private val EXECUTOR = Executors.newSingleThreadScheduledExecutor()
}
init {
EXECUTOR.scheduleWithFixedDelay(
{ cleanUp() },
expireAfterAccess,
expireAfterAccess,
timeUnit
)
}
fun put(key: K, value: V) {
if (cache.size >= maxSize) {
cache.keys.iterator().next()?.let { cache.remove(it) }
}
cache[key] = CacheEntry(value, System.nanoTime())
}
fun get(key: K): V? {
val entry = cache[key] ?: return null
if (System.nanoTime() - entry.lastAccessTime > timeUnit.toNanos(expireAfterAccess)) {
cache.remove(key)
return null
}
entry.lastAccessTime = System.nanoTime()
return entry.value
}
fun remove(key: K) {
cache.remove(key)
}
fun clear() {
cache.clear()
}
private fun cleanUp() {
val now = System.nanoTime()
val expirationTime = timeUnit.toNanos(expireAfterAccess)
val iterator = cache.entries.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
if (now - entry.value.lastAccessTime > expirationTime) {
iterator.remove()
}
}
}
private data class CacheEntry(val value: V, var lastAccessTime: Long)
}
================================================
FILE: android/app/src/main/kotlin/com/network/proxy/vpn/util/TLS.kt
================================================
package com.network.proxy.vpn.util
import java.nio.ByteBuffer
import kotlin.math.min
object TLS {
/**
* 判断是否是TLS Client Hello
*/
fun isTLSClientHello(packetData: ByteBuffer): Boolean {
if (packetData.remaining() < 43) return false
val position = packetData.position()
val data = packetData.array()
if (data[position].toInt() != 0x16 /* handshake */) return false
if (data[1 + position].toInt() != 0x03) return false
return if (data[5 + position].toInt() != 0x01) false else data[9 + position].toInt() == 0x03 && data[10 + position] >= 0x00 && data[1 + position] <= 0x03
}
/**
* 从TLS Client Hello 解析域名
*/
fun getDomain(buffer: ByteBuffer): String? {
var offset = buffer.position()
val limit = buffer.limit()
//TLS Client Hello
if (buffer[offset].toInt() != 0x16) return null
//Skip 43 byte header
offset += 43
if (limit < (offset + 1)) return null
//read session id
val sessionIDLength = buffer[offset++]
offset += sessionIDLength
//read cipher suites
if (offset + 2 > limit) return null
val cipherSuitesLength = buffer.getShort(offset)
offset += 2
offset += cipherSuitesLength
//read Compression method.
if (offset + 1 > limit) return null
val compressionMethodLength = buffer[offset++].toInt() and 0xFF
offset += compressionMethodLength
if (offset > limit) return null
//read Extensions
if (offset + 2 > limit) return null
val extensionsLength = buffer.getShort(offset)
offset += 2
if (offset + extensionsLength > limit) return null
var end: Int = offset + extensionsLength
end = min(end, limit)
while (offset + 4 <= end) {
val extensionType = buffer.getShort(offset)
val extensionLength = buffer.getShort(offset + 2)
offset += 4
//server_name
if (extensionType.toInt() == 0) {
if (offset + 5 > limit) return null
val serverNameListLength = buffer.getShort(offset)
offset += 2
if (offset > limit) return null
if (offset + serverNameListLength > limit) return null
val serverNameType = buffer[offset++]
val serverNameLength = buffer.getShort(offset)
offset += 2
if (offset > limit || serverNameType.toInt() != 0) return null
if (offset + serverNameLength > limit) return null
val serverNameBytes = ByteArray(serverNameLength.toInt())
buffer.get(serverNameBytes)
return String(serverNameBytes)
} else {
offset += extensionLength
}
}
return null
}
}
================================================
FILE: android/app/src/main/res/drawable/launch_background.xml
================================================
================================================
FILE: android/app/src/main/res/drawable-v21/launch_background.xml
================================================
================================================
FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: android/app/src/main/res/values/styles.xml
================================================
ProxyPin Active
抓包正在运行
================================================
FILE: android/app/src/main/res/values-night/styles.xml
================================================
================================================
FILE: android/app/src/profile/AndroidManifest.xml
================================================
================================================
FILE: android/build.gradle
================================================
allprojects {
repositories {
google()
mavenCentral()
}
subprojects {
afterEvaluate { project ->
if (project.hasProperty('android')) {
project.android {
if (namespace == null) {
namespace project.group
}
}
}
}
}
}
rootProject.buildDir = '../build'
subprojects {
afterEvaluate { project ->
if (project.extensions.findByName("android") != null) {
Integer pluginCompileSdk = project.android.compileSdk
if (pluginCompileSdk != null && pluginCompileSdk < 31) {
project.logger.error(
"Warning: Overriding compileSdk version in Flutter plugin: "
+ project.name
+ " from "
+ pluginCompileSdk
+ " to 31 (to work around https://issuetracker.google.com/issues/199180389)."
+ "\nIf there is not a new version of " + project.name + ", consider filing an issue against "
+ project.name
+ " to increase their compileSdk to the latest (otherwise try updating to the latest version)."
)
project.android {
compileSdk 31
}
}
}
}
}
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
================================================
FILE: android/gradle/wrapper/gradle-wrapper.properties
================================================
#Tue Nov 28 00:35:45 CST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: android/gradle.properties
================================================
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
android.nonFinalResIds=false
================================================
FILE: android/settings.gradle
================================================
import java.util.Properties
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.9.1' apply false
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}
include ":app"
================================================
FILE: assets/certs/ca.crt
================================================
-----BEGIN CERTIFICATE-----
MIID4jCCAsqgAwIBAgIJAKcH8Dna4mnZMA0GCSqGSIb3DQEBCwUAMGgxCzAJBgNV
BAYTAkNOMQswCQYDVQQIDAJCSjEQMA4GA1UEBwwHQmVpSmluZzERMA8GA1UECgwI
UHJveHlQaW4xETAPBgNVBAsMCFByb3h5UGluMRQwEgYDVQQDDAtQcm94eVBpbiBD
QTAeFw0yMzA2MjQxNjA2MDlaFw0zMzA2MjExNjA2MTBaMGgxCzAJBgNVBAYTAkNO
MQswCQYDVQQIDAJCSjEQMA4GA1UEBwwHQmVpSmluZzERMA8GA1UECgwIUHJveHlQ
aW4xETAPBgNVBAsMCFByb3h5UGluMRQwEgYDVQQDDAtQcm94eVBpbiBDQTCCASIw
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMRjfFvFDZS+PsdedUNq0Kn5t7RF
NS0iQrZALr4LJm3UwtatHtMEWBb9ptam8pWezxrZPZ81+qnTcaq/To82yus5hJa4
JRk223YWn5JDd4izH4gcnSomhUQ6Ycrc0v+I7UEaHV+bQsleHEfYi2+E1qF+FBhR
veLSPmz2QORd/U4+gDlOptgNWMQ9OTRHsMoDzb8J4SlcBu+s0dnq2WHOM9boGnfk
2wIgE+16uB23epPoYjex8zYGUswh8gNrIzXsr7i9IGtGf67FQYCWOXfZLeGgy0Q0
/r1lwSmywUkNaZIsiGZHveZsLtW93MWMFw0uneEvHsuQV+e8sdLI/633TGcCAwEA
AaOBjjCBizAdBgNVHQ4EFgQU4YXwKkBDFoZY3D81RM25ECSc2qcwHwYDVR0jBBgw
FoAU4YXwKkBDFoZY3D81RM25ECSc2qcwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B
Af8EBAMCAaYwEwYDVR0RBAwwCoIIUHJveHlQaW4wEwYDVR0lBAwwCgYIKwYBBQUH
AwEwDQYJKoZIhvcNAQELBQADggEBAAc2s5TwuOdPdl3gYs121sY+HEMyXfsnVTGV
dIlgjf+a0ECir2bcs64udaCIgBjd/vqhShMeqeQ4GJW7Ypb9556L213xjbLj/ZVU
rgZda6oVd4der8YEHXKLxWAGlZQeeKHhw1lN4PYwxxGaf7/wsM7Dil0JLyOBtJaJ
zNRzVzK9UHASDx0qDQVUBbeYzRviVCjxAGBNM/eNlPDX7m//vgCLxQgcxVdoJvMQ
kSVQddo+d8fxnPAVx77dyX0T/ek7PQOsL6d08TVCdvgv50LwE8f9EMhHVv7zjEv2
0ZSaRQ0pvUnc0ClKXIGeMD71eYeeTz7CGjndxy5bdV/wmoo3Yek=
-----END CERTIFICATE-----
================================================
FILE: assets/certs/ca_key.pem
================================================
-----BEGIN CERTIFICATE-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEY3xbxQ2Uvj7H
XnVDatCp+be0RTUtIkK2QC6+CyZt1MLWrR7TBFgW/abWpvKVns8a2T2fNfqp03Gq
v06PNsrrOYSWuCUZNtt2Fp+SQ3eIsx+IHJ0qJoVEOmHK3NL/iO1BGh1fm0LJXhxH
2ItvhNahfhQYUb3i0j5s9kDkXf1OPoA5TqbYDVjEPTk0R7DKA82/CeEpXAbvrNHZ
6tlhzjPW6Bp35NsCIBPtergdt3qT6GI3sfM2BlLMIfIDayM17K+4vSBrRn+uxUGA
ljl32S3hoMtENP69ZcEpssFJDWmSLIhmR73mbC7VvdzFjBcNLp3hLx7LkFfnvLHS
yP+t90xnAgMBAAECggEAPYnPFhKRRuK2WVLH+/Akop6Vae+l0hbCQMmr2/EygYgB
5bMpzYW29L1W4jw+F5RD4W3hWVpYyY5wN8jqnQXWYA8N9QyO02/VJRPBvNtXQYaf
gs80kFixucdxjVfU5i3J6nR8b9D/BIpw4jKAvtkpSEFxmo1CqyimVw5zFxw8m599
gJX/WeA88l9/4/tGQ24TAZV7OaP+jgqb4hOPC6gB5YHYnGFfuAh8q1Gf3wGnU+3/
pdEDq1UPvMwZ3J7ifTjMHYh2gnT/xQSxOddbtNJfaBx8fUFC4GVEEZ1+j0zc2bOp
/7q+Ab0QXLjMbe3bftMZZqffp2X6NLJipLQcw/HRQQKBgQDonHdqt5ZcsF7+Vsl4
KwmMNAz+jO6j51LU60F7QDhk7hGkCvUF2zJgYSkjlUNl6VS5aGmWTyUR3T3Eqiqs
r81Qao5mxF0MjUU2QKgsw57YG2yASgSPdGqW0PFu1yrxLS51qLIGbp5AuZLULO+M
iTvO1SRm29q45F9f/m0NRda0TQKBgQDYIqGVFcyQvQzGPZc5iOI9we526p+MGEsa
ysRHs8wXJKCiINH2iw1bJGyRCOIZyFQwMRteC174tRnyZpsgTu6wTgaVnTHS8ZNQ
LfjAQsMbs7TItjQF88/thujP15BXzTN7HN1y5kOVCAI7EvLJlZ4jMewfj+aqv2Sb
o5ungsWtgwKBgQCgo2WIqk5JpneDt9WcikQmsc+DfzpSsK6wYeMvxbLsaMh//B0o
NS8+BftOGoeX+qJLBINejTuxcZN1nHqqFSJ59YxwBg2oXGs+wzog59trrMyqb/Nk
SmZNzu/ctvVt5uDd2mlPLddWJZHzuzCXYjKObP2dlxkedIA1H9SZxPA4RQKBgDAS
29/ePmb/NcUuU+GfObtE1HaszxoJGUN3UFsmecG4Cuak6C6vVSQtoNxNnoTfkyI4
+f5cBx7IoWHSQrTX+a1LXZmPolJqGzsdTpPtBZq2yQJPzJh6V4hclpIMP3XYFZhP
nk39O5D9fAmJuGjwF4F6jCulBUh7U7RumqOSqcdjAoGBAKxCtQ0XT0Rlc6B37xTK
/fVYaVbSDISBSVYJTy5vjQi5z+bqUaQrmfeW1z+WoVTeP0ZUgcxTXJbPBVeAC8Wx
oTYfh5yTEu8FCBpBSWWsCteodBBZxXpINLuk9Ex44yxvuFhulugmYzyga+nqufV/
N5e8NEl7aISBW+PK16pnNO0e
-----END CERTIFICATE-----
================================================
FILE: assets/js/fetch.js
================================================
function fetch(url, options) {
options = options || {};
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
const keys = [];
const all = [];
const headers = {};
const response = () => ({
ok: (request.status / 100 | 0) === 2, // 200-299
statusText: request.statusText,
status: request.status,
url: request.responseURL,
body: request.response.body,
text: () => Promise.resolve(request.responseText),
json: () => {
// TODO: review this handle because it may discard \n from json attributes
try {
// console.log('RESPONSE TEXT IN FETCH: ' + request.responseText);
return Promise.resolve(JSON.parse(request.responseText));
} catch (e) {
// console.log('ERROR on fetch parsing JSON: ' + e.message);
return Promise.resolve(request.responseText);
}
},
blob: () => Promise.resolve(request.response.body),
clone: response,
headers: {
...headers,
keys: () => keys,
entries: () => all,
get: n => headers[n.toLowerCase()],
has: n => n.toLowerCase() in headers,
}
});
request.open(options.method || 'get', url, true);
request.onload = () => {
request.getAllResponseHeaders().replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, (m, key, value) => {
keys.push(key = key.toLowerCase());
all.push([key, value]);
headers[key] = headers[key] ? `${headers[key]},${value}` : value;
});
resolve(response());
};
request.onerror = reject;
request.withCredentials = options.credentials == 'include';
if (options.headers) {
if (options.headers.constructor.name == 'Object') {
for (const i in options.headers) {
request.setRequestHeader(i, options.headers[i]);
}
} else { // if it is some Headers pollyfill, the way to iterate is through for of
for (const header of options.headers) {
request.setRequestHeader(header[0], header[1]);
}
}
}
request.send(options.body || null);
});
}
================================================
FILE: devtools_options.yaml
================================================
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
================================================
FILE: distribute_options.yaml
================================================
output: dist/
releases:
- name: release
jobs:
- name: macos-dmg
package:
platform: macos
target: dmg
build_args:
profile: true
- name: windows-exe
package:
platform: windows
target: exe
build_args:
profile: true
- name: windows-msix
package:
platform: windows
target: msix
- name: windows-zip
package:
platform: windows
target: zip
================================================
FILE: ios/.gitignore
================================================
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
================================================
FILE: ios/Flutter/AppFrameworkInfo.plist
================================================
CFBundleDevelopmentRegion
en
CFBundleExecutable
App
CFBundleIdentifier
io.flutter.flutter.app
CFBundleInfoDictionaryVersion
6.0
CFBundleName
App
CFBundlePackageType
FMWK
CFBundleShortVersionString
1.0
CFBundleSignature
????
CFBundleVersion
1.0
MinimumOSVersion
13.0
================================================
FILE: ios/Flutter/Debug.xcconfig
================================================
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
================================================
FILE: ios/Flutter/Release.xcconfig
================================================
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
================================================
FILE: ios/Podfile
================================================
# Uncomment this line to define a global platform for your project
platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
pod 'SnapKit', '~> 5.0.1'
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
# You can remove unused permissions here
# for more information: https://github.com/Baseflow/flutter-permission-handler/blob/main/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h
# e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.camera
'PERMISSION_CAMERA=1',
## dart: PermissionGroup.photos
# 'PERMISSION_PHOTOS=1',
]
end
end
end
================================================
FILE: ios/ProxyPin/Info.plist
================================================
NSExtension
NSExtensionPointIdentifier
com.apple.networkextension.packet-tunnel
NSExtensionPrincipalClass
$(PRODUCT_MODULE_NAME).PacketTunnelProvider
================================================
FILE: ios/ProxyPin/PacketTunnelProvider.swift
================================================
//
// PacketTunnelProvider.swift
// ProxyPin
//
// Created by 汪红恩 on 2023/7/4.
//
import NetworkExtension
import Network
import os.log
class PacketTunnelProvider: NEPacketTunnelProvider {
private var proxyVpnService: ProxyVpnService?
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
NSLog("startTunnel")
guard let conf = (protocolConfiguration as! NETunnelProviderProtocol).providerConfiguration else{
NSLog("[ERROR] No ProtocolConfiguration Found")
exit(EXIT_FAILURE)
}
let host = conf["proxyHost"] as! String
let proxyPort = conf["proxyPort"] as! Int
let ipProxy = conf["ipProxy"] as! Bool? ?? false
// parse proxyPassDomains: accept either [String] or comma-separated String
var proxyPassDomains: [String]? = nil
if let arr = conf["proxyPassDomains"] as? [String] {
proxyPassDomains = arr.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty }
} else if let csv = conf["proxyPassDomains"] as? String {
let list = csv.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty }
proxyPassDomains = list.isEmpty ? nil : list
}
// let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: host)
NSLog(conf.debugDescription)
networkSettings.mtu = 1500
let ipv4Settings = NEIPv4Settings(addresses: ["10.0.0.2"], subnetMasks: ["255.255.255.255"])
if (ipProxy){
ipv4Settings.includedRoutes = [NEIPv4Route.default()]
ipv4Settings.excludedRoutes = [
NEIPv4Route(destinationAddress: "10.0.0.0", subnetMask: "255.0.0.0"),
NEIPv4Route(destinationAddress: "100.64.0.0", subnetMask: "255.192.0.0"),
// NEIPv4Route(destinationAddress: "127.0.0.0", subnetMask: "255.0.0.0"),
NEIPv4Route(destinationAddress: "169.254.0.0", subnetMask: "255.255.0.0"),
NEIPv4Route(destinationAddress: "172.16.0.0", subnetMask: "255.240.0.0"),
NEIPv4Route(destinationAddress: "192.168.0.0", subnetMask: "255.255.0.0"),
NEIPv4Route(destinationAddress: "17.0.0.0", subnetMask: "255.0.0.0"),
]
let dns = "223.5.5.5,8.8.8.8"
let dnsSettings = NEDNSSettings(servers: dns.components(separatedBy: ","))
dnsSettings.matchDomains = [""]
dnsSettings.matchDomainsNoSearch = true
networkSettings.dnsSettings = dnsSettings
}
//http代理
let proxySettings = NEProxySettings()
proxySettings.httpEnabled = true
proxySettings.httpServer = NEProxyServer(address: host, port: proxyPort)
proxySettings.httpsEnabled = true
proxySettings.httpsServer = NEProxyServer(address: host, port: proxyPort)
// If a proxyPassDomains list was provided, use it as the exceptionList so these domains bypass the proxy.
if let pass = proxyPassDomains {
proxySettings.exceptionList = pass
}
proxySettings.matchDomains = [""]
networkSettings.proxySettings = proxySettings
networkSettings.ipv4Settings = ipv4Settings
setTunnelNetworkSettings(networkSettings) { error in
guard error == nil else {
NSLog("startTunnel Encountered an error setting up the network: \(error.debugDescription)")
completionHandler(error)
return
}
if (ipProxy){
let proxyAddress = Network.NWEndpoint.hostPort(host: NWEndpoint.Host(host), port: NWEndpoint.Port(rawValue: UInt16(proxyPort))!)
self.proxyVpnService = ProxyVpnService(packetFlow: self.packetFlow, proxyAddress: proxyAddress)
self.proxyVpnService!.start()
}
completionHandler(nil)
}
}
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
proxyVpnService?.stop()
completionHandler()
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
// Add code here to handle the message.
if let handler = completionHandler {
NSLog("handleAppMessage ", messageData.debugDescription)
handler(messageData)
}
}
override func sleep(completionHandler: @escaping () -> Void) {
// Add code here to get ready to sleep.
completionHandler()
}
override func wake() {
// Add code here to wake up.
}
}
================================================
FILE: ios/ProxyPin/ProxyPin-Bridging-Header.h
================================================
//
// ProxyPin-Bridging-Header.h
// Runner
//
// Created by wanghongen on 2025/5/28.
//
#import "GBPing.h"
================================================
FILE: ios/ProxyPin/ProxyPin.entitlements
================================================
com.apple.security.application-groups
group.com.proxy.pin
com.apple.developer.networking.networkextension
packet-tunnel-provider
com.apple.developer.networking.vpn.api
allow-vpn
================================================
FILE: ios/ProxyPin/vpn/Connection.swift
================================================
//
// Connection.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/17.
//
import Foundation
import Foundation
import Network
import os.log
class Connection{
var nwProtocol: NWProtocol
var sourceIp: UInt32
var sourcePort: UInt16
var destinationIp: UInt32
var destinationPort: UInt16
var channel: NWConnection?
var isInitConnect: Bool = false
var isConnected: Bool = false
var isClosingConnection: Bool = false
var isAbortingConnection: Bool = false
var isAckedToFin: Bool = false
private let connectionCloser: ConnectionManager
init(nwProtocol: NWProtocol, sourceIp: UInt32, sourcePort: UInt16, destinationIp: UInt32, destinationPort: UInt16, connectionCloser: ConnectionManager) {
self.nwProtocol = nwProtocol
self.sourceIp = sourceIp
self.sourcePort = sourcePort
self.destinationIp = destinationIp
self.destinationPort = destinationPort
self.connectionCloser = connectionCloser
}
//发送缓冲区,用于存储要从vpn客户端发送到目标主机的数据
var sendBuffer = Data()
var hasReceivedLastSegment = false
//从客户端接收的最后一个数据包
var lastIpHeader: IP4Header?
var lastTcpHeader: TCPHeader?
var lastUdpHeader: UDPHeader?
var timestampSender = 0
var timestampReplyTo = 0
//从客户端接收的序列
var recSequence: UInt32 = 0
//在tcp选项内的SYN期间由客户端发送
var maxSegmentSize = 0
//跟踪我们发送给客户端的ack,并等待客户端返回ack
var sendUnAck: UInt32 = 0
//发送到客户端的下一个ack
var sendNext: UInt32 = 0
static func getConnectionKey(nwProtocol: NWProtocol, destIp: UInt32, destPort: UInt16, sourceIp: UInt32, sourcePort: UInt16) -> String {
let destIpString = PacketUtil.intToIPAddress(destIp)
let sourceIpString = PacketUtil.intToIPAddress(sourceIp)
return "\(nwProtocol)|\(sourceIpString):\(sourcePort)->\(destIpString):\(destPort)"
}
func closeConnection() {
connectionCloser.closeConnection(connection: self)
}
func addSendData(data: Data) {
self.sendBuffer.append(data)
if (self.channel?.state != .ready) {
os_log("Connection %{public}@ is not ready, cannot send data", log: OSLog.default, type: .debug, self.description)
return
}
self.sendToDestination()
}
//发送到目标服务器的数据
func sendToDestination() {
// os_log("Sending data to destination key %{public}@", log: OSLog.default, type: .debug, self.description)
if (self.sendBuffer.count == 0) {
return
}
let data = self.sendBuffer
self.sendBuffer.removeAll()
self.channel?.send(content: data, completion: .contentProcessed({ error in
if let error = error {
os_log("Failed to send data to destination key %{public}@ error: %{public}@", log: OSLog.default, type: .error, self.description, error.localizedDescription)
self.closeConnection()
}
}))
}
var description: String {
return Connection.getConnectionKey(nwProtocol: nwProtocol, destIp: destinationIp, destPort: destinationPort, sourceIp: sourceIp, sourcePort: sourcePort)
}
}
================================================
FILE: ios/ProxyPin/vpn/ConnectionHandler.swift
================================================
//
// ConnectionHandler.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/16.
//
import Foundation
import NetworkExtension
import Network
import os.log
enum ProtocolType: UInt8 {
case icmp = 1, tcp = 6, udp = 17
}
/// Handles incoming packets and routes them to the appropriate connection.
class ConnectionHandler {
private let manager: ConnectionManager
private let writer: NEPacketTunnelFlow
private let ioService: SocketIOService
init(manager: ConnectionManager, writer: NEPacketTunnelFlow, ioService: SocketIOService) {
self.manager = manager
self.writer = writer
self.ioService = ioService
}
//Handle unknown raw IP packet data
public func handlePacket(packet: Data, version: NSNumber?) {
guard let ipHeader = IPPacketFactory.createIP4Header(data: packet) else {
os_log("Malformed IP packet", log: OSLog.default, type: .error)
return
}
if ipHeader.ipVersion != 4 {
os_log("Unsupported IP version: %d", log: OSLog.default, type: .error, ipHeader.ipVersion)
return
}
// os_log("Handling packet length:%d, protocolNumber: %d", log: OSLog.default, type: .default, packet.count, ipHeader.protocolNumber)
var clientPacketData = packet.subdata(in: IPPacketFactory.IP4_HEADER_SIZE.. Void) {
// objc_sync_enter(lock)
closure()
// objc_sync_exit(lock)
}
private func handleUDPPacket(clientPacketData: Data, ipHeader: IP4Header) {
guard let udpHeader = UDPPacketFactory.createUDPHeader(from: clientPacketData) else {
os_log("Malformed UDP packet", log: OSLog.default, type: .error)
return
}
var connection = manager.getConnection(
nwProtocol: .UDP,
ip: ipHeader.destinationIP,
port: udpHeader.destinationPort,
srcIp: ipHeader.sourceIP,
srcPort: udpHeader.sourcePort
)
let newSession = connection == nil
if connection == nil {
connection = manager.createUDPConnection(
ip: ipHeader.destinationIP,
port: udpHeader.destinationPort,
srcIp: ipHeader.sourceIP,
srcPort: udpHeader.sourcePort
)
}
guard let connection = connection else {
os_log("Failed to create UDP connection", log: OSLog.default, type: .error)
return
}
synchronized(connection) {
os_log("handle UDP Packet %{public}@", log: OSLog.default, type: .default, connection.description)
if newSession {
ioService.registerSession(connection: connection)
}
let payload = clientPacketData.subdata(in: UDPPacketFactory.UDP_HEADER_LENGTH.. 0 {
// os_log("[ConnectionHandler] Received data packet %{public}@ length:%d seq:%u, ack:%u", log: OSLog.default, type: .default, connection.description, dataLength, tcpHeader.sequenceNumber, tcpHeader.ackNumber)
//init proxy
self.initProxyConnect(packetData: tcpHeader.payload!, destinationIP: destinationIP, destinationPort: destinationPort, connection: connection)
manager.addClientData(data: tcpHeader.payload!, connection: connection)
sendAck(ipHeader: ipHeader, tcpHeader: tcpHeader, acceptedDataLength: dataLength, connection: connection)
} else {
// os_log("[ConnectionHandler] Received ACK packet %{public}@ seq:%u, ack:%u", log: OSLog.default, type: .default, connection.description, tcpHeader.sequenceNumber, tcpHeader.ackNumber)
}
acceptAck(tcpHeader: tcpHeader, connection: connection)
if connection.isClosingConnection {
sendFinAck(ipHeader: ipHeader, tcpHeader: tcpHeader, connection: connection)
} else if connection.isAckedToFin && !tcpHeader.isFIN() {
manager.closeConnection(nwProtocol: .TCP, ip: destinationIP, port: destinationPort, srcIp: sourceIP, srcPort: sourcePort)
}
//received the last segment of data from vpn client
if tcpHeader.isPSH() {
// Tell the NIO thread to immediately send data to the destination
pushDataToDestination(connection: connection, tcpHeader: tcpHeader)
} else if tcpHeader.isFIN() {
//fin from vpn client is the last packet
//ack it
ackFinAck(ipHeader: ipHeader, tcpHeader: tcpHeader, connection: connection)
} else if tcpHeader.isRST() {
resetTCPConnection(ip: ipHeader, tcp: tcpHeader)
}
if !connection.isAbortingConnection {
manager.keepSessionAlive(connection: connection)
}
}
} else if tcpHeader.isFIN() {
os_log("Received FIN packet %{public}@:%d seq:%u", log: OSLog.default, type: .default, PacketUtil.intToIPAddress(destinationIP), destinationPort, tcpHeader.sequenceNumber)
//case client sent FIN without ACK
guard let connection = manager.getConnection(nwProtocol: .TCP, ip: destinationIP, port: destinationPort, srcIp: sourceIP, srcPort: sourcePort) else {
ackFinAck(ipHeader: ipHeader, tcpHeader: tcpHeader, connection: nil)
return
}
manager.keepSessionAlive(connection: connection)
} else if tcpHeader.isRST() {
os_log("Received RST packet %{public}@:%d seq:%u", log: OSLog.default, type: .debug, PacketUtil.intToIPAddress(destinationIP), destinationPort, tcpHeader.sequenceNumber)
resetTCPConnection(ip: ipHeader, tcp: tcpHeader)
} else {
os_log("Unknown TCP flag", log: OSLog.default, type: .error)
}
}
private func initProxyConnect(
packetData: Data,
destinationIP: UInt32,
destinationPort: UInt16,
connection: Connection
) {
guard !connection.isInitConnect else {
return
}
connection.isInitConnect = true
let supportsProtocol = supportsProtocol(packetData: packetData)
let endpoint: Network.NWEndpoint
if (supportsProtocol && manager.proxyAddress != nil) {
endpoint = manager.proxyAddress!
} else {
let ipString = PacketUtil.intToIPAddress(destinationIP)
endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(ipString), port: NWEndpoint.Port(rawValue: destinationPort)!)
}
// 使用 TCP 协议
let parameters = NWParameters.tcp
let nwConnection = NWConnection(to: endpoint, using: parameters)
connection.channel = nwConnection
connection.isInitConnect = true
self.ioService.registerSession(connection: connection)
}
private let methods: [String] = [
"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE", "CONNECT", "PROPFIND", "REPORT"
]
private func supportsProtocol(packetData: Data) -> Bool {
let position = packetData.startIndex
// 判断是否是 SSL 握手
if TLS.isTLSClientHello(packetData: packetData) {
return true
}
// 检查是否包含 HTTP 方法
for method in methods {
if packetData.count - position < method.count {
continue
}
let range = position..<(position + method.count)
if let substring = String(data: packetData.subdata(in: range), encoding: .utf8),
method.caseInsensitiveCompare(substring) == .orderedSame {
return true
}
}
return false
}
//set connection as aborting so that background worker will close it.
func resetTCPConnection(ip: IP4Header, tcp: TCPHeader) {
let session = manager.getConnection(nwProtocol: .TCP, ip: ip.destinationIP, port: tcp.destinationPort, srcIp: ip.sourceIP, srcPort: tcp.sourcePort)
if let session = session {
session.isAbortingConnection = true
}
}
func ackFinAck(ipHeader: IP4Header, tcpHeader: TCPHeader, connection: Connection?) {
let ackNumber = tcpHeader.sequenceNumber + 1
let seqNumber = tcpHeader.ackNumber
let finAckData = TCPPacketFactory.createFinAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber, seqToClient: seqNumber, isFin: true, isAck: true)
write(data: finAckData)
// os_log("Sent FIN-ACK packet ack# %{public}d, seq# %{public}d", log: OSLog.default, type: .default, ackNumber, seqNumber)
if let connection = connection {
manager.closeConnection(connection: connection)
}
}
func pushDataToDestination(connection: Connection, tcpHeader: TCPHeader) {
connection.timestampReplyTo = tcpHeader.timeStampSender
connection.timestampSender = Int(Date().timeIntervalSince1970)
}
func sendFinAck(ipHeader: IP4Header, tcpHeader: TCPHeader, connection: Connection) {
let ackNumber = tcpHeader.sequenceNumber
let seqNumber = tcpHeader.ackNumber
let finAckData = TCPPacketFactory.createFinAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber, seqToClient: seqNumber, isFin: true, isAck: false)
write(data: finAckData)
connection.sendNext = seqNumber + 1
connection.isClosingConnection = false
}
//acknowledge a packet.
func acceptAck(tcpHeader: TCPHeader, connection: Connection) {
let isCorrupted = PacketUtil.isPacketCorrupted(tcpHeader: tcpHeader)
if isCorrupted {
os_log("Packet is corrupted", log: OSLog.default, type: .error)
}
if (tcpHeader.sequenceNumber > connection.recSequence) {
connection.recSequence = tcpHeader.sequenceNumber
}
if tcpHeader.ackNumber >= connection.sendUnAck - 1 || tcpHeader.ackNumber == connection.sendNext {
connection.sendUnAck = tcpHeader.ackNumber
connection.timestampReplyTo = tcpHeader.timeStampSender
connection.timestampSender = Int(Date().timeIntervalSince1970)
} else {
os_log("%{public}@ Not accepting ack# %d, it should be: %d", log: OSLog.default, type: .error, connection.description ,tcpHeader.ackNumber, connection.sendNext)
os_log("%{public}@ Previous sendUnAck: %d", log: OSLog.default, type: .error, connection.description, connection.sendUnAck)
}
}
func sendAckForDisorder(ipHeader: IP4Header, tcpHeader: TCPHeader, acceptedDataLength: Int) {
let ackNumber = tcpHeader.sequenceNumber + UInt32(acceptedDataLength)
// os_log("Sent disorder ack, ack# %{public}d", log: OSLog.default, type: .debug, ackNumber)
let ackData = TCPPacketFactory.createResponseAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber)
write(data: ackData)
}
func sendAck(ipHeader: IP4Header, tcpHeader: TCPHeader, acceptedDataLength: Int, connection: Connection) {
synchronized(connection) {
let ackNumber = (tcpHeader.sequenceNumber + UInt32(acceptedDataLength)) % UInt32.max
connection.recSequence = ackNumber
let ackData = TCPPacketFactory.createResponseAckData(ipHeader: ipHeader, tcpHeader: tcpHeader, ackToClient: ackNumber)
self.write(data: ackData)
// os_log("[ConnectionHandler] Sent ACK packet to client %{public}@ ack: %u", log: OSLog.default, type: .default, connection.description, ackNumber)
}
}
private func sendLastAck(ip: IP4Header, tcp: TCPHeader) {
let data = TCPPacketFactory.createResponseAckData(ipHeader: ip, tcpHeader: tcp, ackToClient: tcp.sequenceNumber + 1)
self.write(data: data)
os_log("Sent last ACK Packet to client with dest => %{public}@:%{public}d", log: OSLog.default, type: .debug, PacketUtil.intToIPAddress(ip.destinationIP), tcp.destinationPort)
}
private func sendRstPacket(ip: IP4Header, tcp: TCPHeader, dataLength: Int) {
let data = TCPPacketFactory.createRstData(ipHeader: ip, tcpHeader: tcp, dataLength: dataLength)
self.write(data: data)
os_log("Sent RST Packet to client with dest => %{public}@:%{public}d", log: OSLog.default, type: .debug, PacketUtil.intToIPAddress(ip.destinationIP), tcp.destinationPort)
}
//create a new client's session and SYN-ACK packet data to respond to client
private func replySynAck(ipHeader: IP4Header, tcpHeader: TCPHeader) -> Void {
ipHeader.identification = 0
let packet = TCPPacketFactory.createSynAckPacketData(ipHeader: ipHeader, tcpHeader: tcpHeader)
guard let tcpTransport = packet.transportHeader as? TCPHeader else {
os_log("Failed to extract TCP header from packet", log: OSLog.default, type: .error)
return
}
let connection = manager.createTCPConnection(
ip: ipHeader.destinationIP,
port: tcpHeader.destinationPort,
srcIp: ipHeader.sourceIP,
srcPort: tcpHeader.sourcePort
)
if connection.lastIpHeader != nil {
resendAck(connection: connection)
return
}
synchronized(connection) {
connection.maxSegmentSize = Int(tcpTransport.maxSegmentSize)
connection.sendUnAck = tcpTransport.sequenceNumber
connection.sendNext = tcpTransport.sequenceNumber + 1
//client initial sequence has been incremented by 1 and set to ack
connection.recSequence = tcpTransport.ackNumber
connection.lastIpHeader = ipHeader
connection.lastTcpHeader = tcpHeader
if connection.isInitConnect {
self.ioService.registerSession(connection: connection)
}
self.write(data: packet.buffer)
// os_log("SYN-ACK %{public}@ packet length:%d sent ack:%u", log: OSLog.default, type: .default, connection.description, packet.buffer.count, tcpTransport.ackNumber)
}
}
/**
* resend the last acknowledgment packet to VPN client, e.g. when an unexpected out of order
* packet arrives.
*/
private func resendAck(connection: Connection) {
let data = TCPPacketFactory.createResponseAckData(
ipHeader: connection.lastIpHeader!,
tcpHeader: connection.lastTcpHeader!,
ackToClient: connection.recSequence
)
// os_log("Resending ACK packet %{public}@ ackToClient: %d", log: OSLog.default, type: .default, connection.description, connection.recSequence)
self.write(data: data)
}
private func write(data: Data) {
self.writer.writePackets([data], withProtocols: [NSNumber(value: AF_INET)])
}
private func handleICMPPacket(clientPacketData: inout Data, ipHeader: IP4Header) {
guard let requestPacket = ICMPPacketFactory.parseICMPPacket(&clientPacketData) else {
os_log("Failed to parse ICMP packet", log: OSLog.default, type: .error)
return
}
// os_log("Handling ICMP packet type: %d", log: OSLog.default, type: .default, requestPacket.type)
if requestPacket.type == ICMPPacket.DESTINATION_UNREACHABLE_TYPE {
// This is a packet from the phone, telling somebody that a destination is unreachable.
// Might be caused by issues on our end, but it's unclear what kind of issues. Regardless,
// we can't send ICMP messages ourselves or react usefully, so we drop these silently.
return
} else if requestPacket.type != ICMPPacket.ECHO_REQUEST_TYPE {
// We only actually support outgoing ping packets. Loudly drop anything else:
os_log("Unknown ICMP type: %d", log: OSLog.default, type: .error, requestPacket.type)
return
}
QueueFactory.instance.getQueue().async {
if !self.isReachable(ipAddress: PacketUtil.intToIPAddress(ipHeader.destinationIP)) {
os_log("Failed ping, ignoring", log: OSLog.default, type: .default)
return
}
let response = ICMPPacketFactory.buildSuccessPacket(requestPacket)
// Flip the address
let destination = ipHeader.destinationIP
let source = ipHeader.sourceIP
ipHeader.sourceIP = destination
ipHeader.destinationIP = source
let responseData = ICMPPacketFactory.packetToBuffer(ipHeader: ipHeader, packet: response)
os_log("Successful ping response", log: OSLog.default, type: .default)
self.write(data: responseData)
}
}
private func isReachable(ipAddress: String) -> Bool {
do {
return true
// return try InetAddress.getByName(ipAddress).isReachable(timeout: 10000)
} catch {
return false
}
}
}
================================================
FILE: ios/ProxyPin/vpn/ConnectionManager.swift
================================================
//
// ConnectionManager.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/16.
//
import Foundation
import Network
import os.log
//管理VPN客户端的连接
class ConnectionManager : CloseableConnection{
//static let instance = ConnectionManager()
private var table: [String: Connection] = [:]
public var proxyAddress: NWEndpoint?
private let defaultPorts: [UInt16] = [80, 443, 8080, 8088, 8888, 9000]
func getConnection(nwProtocol: NWProtocol, ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) -> Connection? {
let key = Connection.getConnectionKey(nwProtocol: nwProtocol, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort)
return getConnectionByKey(key: key)
}
func getConnectionByKey(key: String) -> Connection? {
return table[key]
}
func createTCPConnection(ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) -> Connection {
let key = Connection.getConnectionKey(nwProtocol: .TCP, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort)
if let existingConnection = table[key] {
return existingConnection
}
let connection = Connection(nwProtocol: .TCP, sourceIp: srcIp, sourcePort: srcPort, destinationIp: ip, destinationPort: port, connectionCloser: self)
let ipString = PacketUtil.intToIPAddress(ip)
let endpoint: NWEndpoint
if (proxyAddress == nil || !defaultPorts.contains(port) || isPrivateIP(ipString)) {
endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(ipString), port: NWEndpoint.Port(rawValue: port)!)
// 使用 TCP 协议
let parameters = NWParameters.tcp
let nwConnection = NWConnection(to: endpoint, using: parameters)
connection.channel = nwConnection
connection.isInitConnect = true
}
self.table[key] = connection
os_log("Created TCP connection %{public}@", log: OSLog.default, type: .default, key)
return connection
}
private func isPrivateIP(_ ip: String) -> Bool {
return ip.hasPrefix("10.") ||
ip.hasPrefix("172.") && (16...31).contains(Int(ip.split(separator: ".")[1]) ?? -1) ||
ip.hasPrefix("192.168.")
}
func createUDPConnection(ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) -> Connection {
let key = Connection.getConnectionKey(nwProtocol: .UDP, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort)
if let existingConnection = table[key] {
return existingConnection
}
let connection = Connection(nwProtocol: .UDP, sourceIp: srcIp, sourcePort: srcPort, destinationIp: ip, destinationPort: port, connectionCloser: self)
let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host((PacketUtil.intToIPAddress(ip))), port: NWEndpoint.Port(rawValue: port)!)
let nwConnection = NWConnection(to: endpoint, using: .udp)
connection.channel = nwConnection
os_log("Created UDP connection %{public}@", log: OSLog.default, type: .default, key)
self.table[key] = connection
return connection
}
func closeConnection(connection: Connection) {
closeConnection(
nwProtocol: connection.nwProtocol, ip: connection.destinationIp, port: connection.destinationPort,
srcIp: connection.sourceIp, srcPort: connection.sourcePort
)
}
// 从内存中删除连接,然后关闭套接字。
func closeConnection(nwProtocol: NWProtocol, ip: UInt32, port: UInt16, srcIp: UInt32, srcPort: UInt16) {
let key = Connection.getConnectionKey(nwProtocol: nwProtocol, destIp: ip, destPort: port, sourceIp: srcIp, sourcePort: srcPort)
if let connection = self.table.removeValue(forKey: key) {
if connection.channel?.state != .cancelled {
connection.channel?.cancel()
os_log("Closed connection %{public}@", log: OSLog.default, type: .debug, key)
} else {
os_log("Connection %{public}@ is already cancelled", log: OSLog.default, type: .debug, key)
}
}
}
//添加来自客户端的数据,该数据稍后将在接收到PSH标志时发送到目的服务器。
func addClientData(data: Data, connection: Connection) {
guard data.count > 0 else {
return
}
connection.addSendData(data: data)
}
func keepSessionAlive(connection: Connection) {
let key = Connection.getConnectionKey(
nwProtocol: connection.nwProtocol,
destIp: connection.destinationIp,
destPort: connection.destinationPort,
sourceIp: connection.sourceIp,
sourcePort: connection.sourcePort
)
self.table[key] = connection
}
}
================================================
FILE: ios/ProxyPin/vpn/NWProtocol.swift
================================================
//
// NWProtocol.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/17.
//
import Foundation
public enum NWProtocol {
case TCP,UDP
}
================================================
FILE: ios/ProxyPin/vpn/ProxyVpnService.swift
================================================
//
// ProxyService.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/17.
//
import Foundation
import NetworkExtension
import Network
import os.log
class ProxyVpnService {
private var packetFlow: NEPacketTunnelFlow
private var connectionHandler: ConnectionHandler
private var socketIOService: SocketIOService
private var isRunning = true;
init(packetFlow: NEPacketTunnelFlow, proxyAddress: Network.NWEndpoint?) {
self.packetFlow = packetFlow
self.socketIOService = SocketIOService(clientPacketWriter: packetFlow)
let manager = ConnectionManager()
manager.proxyAddress = proxyAddress
self.connectionHandler = ConnectionHandler(manager: manager, writer: packetFlow, ioService: socketIOService)
}
/**
Start processing packets, this should be called after registering all IP stacks.
A stopped interface should never start again. Create a new interface instead.
*/
func start() {
isRunning = true;
self.readPackets()
}
func stop() {
isRunning = false;
self.socketIOService.stop()
}
func readPackets() -> Void {
if (!isRunning) {
return
}
self.packetFlow.readPackets { (packets, protocols) in
// os_log("Read %d packets", packets.count)
for (i, packet) in packets.enumerated() {
self.connectionHandler.handlePacket(packet: packet, version: protocols[i])
}
self.readPackets()
}
}
}
================================================
FILE: ios/ProxyPin/vpn/QueueFactory.swift
================================================
//
// QueueFactory.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/17.
//
import Foundation
class QueueFactory {
static let instance = QueueFactory()
private let queue: DispatchQueue
private init() {
queue = DispatchQueue(label: "com.network.ProxyPin.queue")
}
func getQueue() -> DispatchQueue {
return queue
}
func executeAsync(block: @escaping () -> Void) {
queue.async {
block()
}
}
}
================================================
FILE: ios/ProxyPin/vpn/ping/GBPing.h
================================================
//
// GBPing.h
// GBPing
//
// Created by Luka Mirosevic on 05/11/2012.
// Copyright (c) 2012 Goonbee. All rights reserved.
//
#import
#import "GBPingSummary.h"
@class GBPingSummary;
@protocol GBPingDelegate;
NS_ASSUME_NONNULL_BEGIN
typedef void(^StartupCallback)(BOOL success, NSError * _Nullable error);
@interface GBPing : NSObject
@property (weak, nonatomic, nullable) id delegate;
@property (copy, nonatomic, nullable) NSString *host;
@property (assign, atomic) NSTimeInterval pingPeriod;
@property (assign, atomic) NSTimeInterval timeout;
@property (assign, atomic) NSUInteger payloadSize;
@property (assign, atomic) NSUInteger ttl;
@property (assign, atomic) NSUInteger count;
@property (assign, atomic, readonly) BOOL isPinging;
@property (assign, atomic, readonly) BOOL isReady;
@property (assign, atomic) BOOL useIpv4;
@property (assign, atomic) BOOL useIpv6;
@property (assign, atomic) BOOL debug;
-(void)setupWithBlock:(StartupCallback)callback;
-(void)startPinging;
-(void)stop;
@end
@protocol GBPingDelegate
@optional
-(void)ping:(GBPing *)pinger didFinishWithTime:(NSTimeInterval)time;
-(void)ping:(GBPing *)pinger didFailWithError:(NSError *)error;
-(void)ping:(GBPing *)pinger didSendPingWithSummary:(GBPingSummary *)summary;
-(void)ping:(GBPing *)pinger didFailToSendPingWithSummary:(GBPingSummary *)summary error:(NSError *)error;
-(void)ping:(GBPing *)pinger didTimeoutWithSummary:(GBPingSummary *)summary;
-(void)ping:(GBPing *)pinger didReceiveReplyWithSummary:(GBPingSummary *)summary;
-(void)ping:(GBPing *)pinger didReceiveUnexpectedReplyWithSummary:(GBPingSummary *)summary;
@end
NS_ASSUME_NONNULL_END
================================================
FILE: ios/ProxyPin/vpn/ping/GBPing.m
================================================
//
// GBPing.m
// GBPing
//
// Created by Luka Mirosevic on 05/11/2012.
// Copyright (c) 2012 Goonbee. All rights reserved.
//
#import "GBPing.h"
#if TARGET_OS_EMBEDDED || TARGET_IPHONE_SIMULATOR
#import
#else
#import
#endif
#import "ICMPHeader.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
static NSTimeInterval const kPendingPingsCleanupGrace = 1.0;
static NSUInteger const kDefaultPayloadSize = 56;
static NSUInteger const kDefaultTTL = 49;
static NSTimeInterval const kDefaultPingPeriod = 1.0;
static NSTimeInterval const kDefaultTimeout = 2.0;
@interface GBPing ()
@property (assign, atomic) int socket;
@property (strong, nonatomic) NSData *hostAddress;
@property (strong, nonatomic) NSString *hostAddressString;
@property (assign, nonatomic) uint16_t identifier;
@property (assign, nonatomic) NSUInteger counter;
@property (assign, atomic, readwrite) BOOL isPinging;
@property (assign, atomic, readwrite) BOOL isReady;
@property (assign, nonatomic) NSUInteger nextSequenceNumber;
@property (strong, atomic) NSMutableDictionary *pendingPings;
@property (strong, nonatomic) NSMutableDictionary *timeoutTimers;
@property (strong, nonatomic) dispatch_queue_t setupQueue;
@property (assign, atomic) BOOL isStopped;
@end
@implementation GBPing {
NSUInteger _payloadSize;
NSUInteger _ttl;
NSUInteger _count;
NSTimeInterval _timeout;
NSTimeInterval _pingPeriod;
NSTimeInterval _endTime;
}
#pragma mark - custom acc
-(void)setTimeout:(NSTimeInterval)timeout {
@synchronized(self) {
if (self.isPinging) {
if (self.debug) {
NSLog(@"GBPing: can't set timeout while pinger is running.");
}
}
else {
_timeout = timeout;
}
}
}
-(NSTimeInterval)timeout {
@synchronized(self) {
if (!_timeout) {
return kDefaultTimeout;
}
else {
return _timeout;
}
}
}
-(void)setTtl:(NSUInteger)ttl {
@synchronized(self) {
if (self.isPinging) {
if (self.debug) {
NSLog(@"GBPing: can't set ttl while pinger is running.");
}
}
else {
_ttl = ttl;
}
}
}
-(NSUInteger)ttl {
@synchronized(self) {
if (!_ttl) {
return kDefaultTTL;
}
else {
return _ttl;
}
}
}
-(void)setCount:(NSUInteger)count {
@synchronized(self) {
if (self.isPinging) {
if (self.debug) {
NSLog(@"GBPing: can't set count while pinger is running.");
}
}
else {
_count = count;
}
}
}
-(NSUInteger)count {
@synchronized(self) {
if (!_count) {
return 0;
}
else {
return _count;
}
}
}
-(void)setPayloadSize:(NSUInteger)payloadSize {
@synchronized(self) {
if (self.isPinging) {
if (self.debug) {
NSLog(@"GBPing: can't set payload size while pinger is running.");
}
}
else {
_payloadSize = payloadSize;
}
}
}
-(NSUInteger)payloadSize {
@synchronized(self) {
if (!_payloadSize) {
return kDefaultPayloadSize;
}
else {
return _payloadSize;
}
}
}
-(void)setPingPeriod:(NSTimeInterval)pingPeriod {
@synchronized(self) {
if (self.isPinging) {
if (self.debug) {
NSLog(@"GBPing: can't set pingPeriod while pinger is running.");
}
}
else {
_pingPeriod = pingPeriod;
}
}
}
-(NSTimeInterval)pingPeriod {
@synchronized(self) {
if (!_pingPeriod) {
return (NSTimeInterval)kDefaultPingPeriod;
}
else {
return _pingPeriod;
}
}
}
#pragma mark - core pinging methods
-(void)setupWithBlock:(StartupCallback)callback {
//error out of its already setup
if (self.isReady) {
if (self.debug) {
NSLog(@"GBPing: Can't setup, already setup.");
}
//notify about error and return
dispatch_async(dispatch_get_main_queue(), ^{
callback(NO, nil);
});
return;
}
//error out if no host is set
if (!self.host) {
if (self.debug) {
NSLog(@"GBPing: set host before attempting to start.");
}
//notify about error and return
dispatch_async(dispatch_get_main_queue(), ^{
callback(NO, nil);
});
return;
}
//set up data structs
self.nextSequenceNumber = 0;
@synchronized (self) {
self.pendingPings = [[NSMutableDictionary alloc] init];
self.timeoutTimers = [[NSMutableDictionary alloc] init];
}
dispatch_async(self.setupQueue, ^{
CFStreamError streamError;
BOOL success;
CFHostRef hostRef = CFHostCreateWithName(NULL, (__bridge CFStringRef)self.host);
/*
* CFHostCreateWithName will return a null result in certain cases.
* CFHostStartInfoResolution will return YES if _hostRef is null.
*/
if (hostRef) {
success = CFHostStartInfoResolution(hostRef, kCFHostAddresses, &streamError);
} else {
success = NO;
}
if (!success) {
//construct an error
NSDictionary *userInfo;
NSError *error;
if (hostRef && streamError.domain == kCFStreamErrorDomainNetDB) {
userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInteger:streamError.error], kCFGetAddrInfoFailureKey,
nil
];
}
else {
userInfo = nil;
}
error = [NSError errorWithDomain:(NSString *)kCFErrorDomainCFNetwork code:kCFHostErrorUnknown userInfo:userInfo];
//clean up so far
[self stop];
//notify about error and return
dispatch_async(dispatch_get_main_queue(), ^{
callback(NO, error);
});
//just incase
if (hostRef) {
CFRelease(hostRef);
}
return;
}
//get the first IPv4 or IPv6 address
Boolean resolved;
NSArray *addresses = (__bridge NSArray *)CFHostGetAddressing(hostRef, &resolved);
if (resolved && (addresses != nil)) {
resolved = false;
for (NSData *address in addresses) {
const struct sockaddr *anAddrPtr = (const struct sockaddr *)[address bytes];
if ([address length] >= sizeof(struct sockaddr) &&
((self.useIpv4 && anAddrPtr->sa_family == AF_INET) ||
(self.useIpv6 && anAddrPtr->sa_family == AF_INET6)) ) {
resolved = true;
self.hostAddress = address;
self.hostAddressString = [self ntop:(struct sockaddr *)anAddrPtr len:(socklen_t)address.length];
break;
}
}
}
//we can stop host resolution now
if (hostRef) {
CFRelease(hostRef);
}
//if an error occurred during resolution
if (!resolved) {
//stop
[self stop];
//notify about error and return
dispatch_async(dispatch_get_main_queue(), ^{
callback(NO, [NSError errorWithDomain:(NSString *)kCFErrorDomainCFNetwork code:kCFHostErrorHostNotFound userInfo:nil]);
});
return;
}
//set up socket
int err = 0;
switch (self.hostAddressFamily) {
case AF_INET: {
self.socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
if (self.socket < 0) {
err = errno;
}
} break;
case AF_INET6: {
self.socket = socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6);
if (self.socket < 0) {
err = errno;
}
} break;
default: {
err = EPROTONOSUPPORT;
} break;
}
//couldnt setup socket
if (err) {
//clean up so far
[self stop];
//notify about error and close
dispatch_async(dispatch_get_main_queue(), ^{
callback(NO, [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil]);
});
return;
}
//set ttl on the socket
if (self.ttl) {
u_char ttlForSockOpt = (u_char)self.ttl;
setsockopt(self.socket, IPPROTO_IP, IP_TTL, &ttlForSockOpt, sizeof(NSUInteger));
}
//we are ready now
self.isReady = YES;
//notify that we are ready
dispatch_async(dispatch_get_main_queue(), ^{
callback(YES, nil);
});
});
self.isStopped = NO;
}
-(void)startPinging {
if (self.isReady && !self.isPinging) {
//go into infinite listenloop on a new thread (listenThread)
NSThread *listenThread = [[NSThread alloc] initWithTarget:self selector:@selector(listenLoop) object:nil];
listenThread.name = @"listenThread";
//set up loop that sends packets on a new thread (sendThread)
NSThread *sendThread = [[NSThread alloc] initWithTarget:self selector:@selector(sendLoop) object:nil];
sendThread.name = @"sendThread";
//we're pinging now
self.isPinging = YES;
[listenThread start];
[sendThread start];
}
}
-(void)listenLoop {
@autoreleasepool {
while (self.isPinging) {
[self listenOnce];
}
}
}
-(void)listenOnce {
int err;
struct sockaddr_storage addr;
socklen_t addrLen;
ssize_t bytesRead;
void * buffer;
enum { kBufferSize = 65535 };
buffer = malloc(kBufferSize);
if (buffer == nil) {
err = errno;
return;
}
//read the data.
addrLen = sizeof(addr);
bytesRead = recvfrom(self.socket, buffer, kBufferSize, 0, (struct sockaddr *)&addr, &addrLen);
err = 0;
if (bytesRead < 0) {
err = errno;
}
//process the data we read.
if (bytesRead > 0) {
_endTime = CFAbsoluteTimeGetCurrent();
struct sockaddr_in *sin = (struct sockaddr_in *)&addr;
NSString *host = [self ntop:(struct sockaddr *)&addr len:addrLen];
if([host isEqualToString:self.hostAddressString]) { // only make sense where received packet comes from expected source
NSDate *receiveDate = [NSDate date];
NSMutableData *packet;
packet = [NSMutableData dataWithBytes:buffer length:(NSUInteger) bytesRead];
if (packet == nil) {
err = errno;
return;
}
//complete the ping summary
const struct ICMPHeader *headerPointer;
if (sin->sin_family == AF_INET) {
headerPointer = [[self class] icmp4InPacket:packet];
} else {
headerPointer = (const struct ICMPHeader *)[packet bytes];
}
NSUInteger seqNo = (NSUInteger)OSSwapBigToHostInt16(headerPointer->sequenceNumber);
NSNumber *key = @(seqNo);
GBPingSummary *pingSummary;
@synchronized (self) {
pingSummary = [(GBPingSummary *)self.pendingPings[key] copy];
}
if (pingSummary) {
if ([self isValidPingResponsePacket:packet]) {
pingSummary.receiveDate = receiveDate;
if (sin->sin_family == AF_INET) {
//set ttl from response (different servers may respond with different ttls)
const struct IPHeader *ipPtr;
if ([packet length] >= sizeof(IPHeader)) {
ipPtr = (const IPHeader *)[packet bytes];
pingSummary.ttl = ipPtr->timeToLive;
}
}
pingSummary.status = GBPingStatusSuccess;
//invalidate the timeouttimer
@synchronized (self) {
NSTimer *timer = self.timeoutTimers[key];
[timer invalidate];
[self.timeoutTimers removeObjectForKey:key];
}
dispatch_async(dispatch_get_main_queue(), ^{
//notify delegate
if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didReceiveReplyWithSummary:)] ) {
[self.delegate ping:self didReceiveReplyWithSummary:[pingSummary copy]];
}
});
}
else {
pingSummary.status = GBPingStatusFail;
dispatch_async(dispatch_get_main_queue(), ^{
if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didReceiveUnexpectedReplyWithSummary:)] ) {
[self.delegate ping:self didReceiveUnexpectedReplyWithSummary:[pingSummary copy]];
}
});
}
}
}
}
else {
//we failed to read the data, so shut everything down.
if (err == 0) {
err = EPIPE;
}
@synchronized(self) {
if (!self.isStopped) {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didFailWithError:)] ) {
[self.delegate ping:self didFailWithError:[NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil]];
}
});
}
}
//stop the whole thing
[self stop];
}
free(buffer);
}
-(void)sendLoop {
@autoreleasepool {
self.counter = _count;
BOOL stopping = NO;
NSTimeInterval startTime = CFAbsoluteTimeGetCurrent();
_endTime = 0;
while (self.isPinging) {
[self sendPing];
if (_count > 0) {
self.counter -= 1;
if (self.counter == 0) {
stopping = YES;
}
}
NSTimeInterval runUntil = CFAbsoluteTimeGetCurrent() + (stopping ? self.timeout : self.pingPeriod);
NSTimeInterval time = 0;
while (runUntil > time) {
NSDate *runUntilDate = [NSDate dateWithTimeIntervalSinceReferenceDate:runUntil];
[[NSRunLoop currentRunLoop] runUntilDate:runUntilDate];
time = CFAbsoluteTimeGetCurrent();
}
if (stopping) {
break;
}
}
[self stop];
dispatch_async(dispatch_get_main_queue(), ^{
if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didFinishWithTime:)] ) {
NSTimeInterval interval = 0;
if (self->_endTime > 0) {
interval = self->_endTime - startTime;
}
[self.delegate ping:self didFinishWithTime:interval];
}
});
}
}
-(void)sendPing {
if (self.isPinging) {
int err;
NSData *packet;
ssize_t bytesSent;
// Construct the ping packet.
NSData *payload = [self generateDataWithLength:(self.payloadSize)];
switch (self.hostAddressFamily) {
case AF_INET: {
packet = [self pingPacketWithType:kICMPv4TypeEchoRequest payload:payload requiresChecksum:YES];
} break;
case AF_INET6: {
packet = [self pingPacketWithType:kICMPv6TypeEchoRequest payload:payload requiresChecksum:NO];
} break;
default: {
err = errno;
return;
} break;
}
// this is our ping summary
GBPingSummary *newPingSummary = [GBPingSummary new];
// Send the packet.
if (self.socket == 0) {
bytesSent = -1;
err = EBADF;
}
else {
//record the send date
NSDate *sendDate = [NSDate date];
//construct ping summary, as much as it can
newPingSummary.sequenceNumber = self.nextSequenceNumber;
newPingSummary.host = self.host;
newPingSummary.ip = self.hostAddressString;
newPingSummary.sendDate = sendDate;
newPingSummary.ttl = self.ttl;
newPingSummary.payloadSize = self.payloadSize;
newPingSummary.status = GBPingStatusPending;
//add it to pending pings
NSNumber *key = @(self.nextSequenceNumber);
@synchronized (self) {
self.pendingPings[key] = newPingSummary;
}
//increment sequence number
self.nextSequenceNumber += 1;
//we create a copy, this one will be passed out to other threads
GBPingSummary *pingSummaryCopy = [newPingSummary copy];
//we need to clean up our list of pending pings, and we do that after the timeout has elapsed (+ some grace period)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((self.timeout + kPendingPingsCleanupGrace) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//remove the ping from the pending list
@synchronized (self) {
[self.pendingPings removeObjectForKey:key];
}
});
//add a timeout timer
//add a timeout timer
NSTimer *timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:self.timeout
target:[NSBlockOperation blockOperationWithBlock:^{
newPingSummary.status = GBPingStatusFail;
self->_endTime = CFAbsoluteTimeGetCurrent();
//notify about the failure
dispatch_async(dispatch_get_main_queue(), ^{
if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didTimeoutWithSummary:)]) {
[self.delegate ping:self didTimeoutWithSummary:pingSummaryCopy];
}
});
//remove the timer itself from the timers list
//lm make sure that the timer list doesnt grow and these removals actually work... try logging the count of the timeoutTimers when stopping the pinger
@synchronized (self) {
[self.timeoutTimers removeObjectForKey:key];
}
}]
selector:@selector(main)
userInfo:nil
repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:timeoutTimer forMode:NSRunLoopCommonModes];
//keep a local ref to it
if (self.timeoutTimers) {
@synchronized (self) {
self.timeoutTimers[key] = timeoutTimer;
}
}
//notify delegate about this
dispatch_async(dispatch_get_main_queue(), ^{
if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didSendPingWithSummary:)]) {
[self.delegate ping:self didSendPingWithSummary:pingSummaryCopy];
}
});
bytesSent = sendto(
self.socket,
[packet bytes],
[packet length],
0,
(struct sockaddr *) [self.hostAddress bytes],
(socklen_t) [self.hostAddress length]
);
err = 0;
if (bytesSent < 0) {
err = errno;
}
}
// This is after the sending
//successfully sent
if ((bytesSent > 0) && (((NSUInteger) bytesSent) == [packet length])) {
//noop, we already notified delegate about sending of the ping
}
//failed to send
else {
//complete the error
if (err == 0) {
err = ENOBUFS; // This is not a hugely descriptor error, alas.
}
//little log
if (self.debug) {
NSLog(@"GBPing: failed to send packet with error code: %d", err);
}
//change status
newPingSummary.status = GBPingStatusFail;
GBPingSummary *pingSummaryCopyAfterFailure = [newPingSummary copy];
//notify delegate
dispatch_async(dispatch_get_main_queue(), ^{
if (self.delegate && [self.delegate respondsToSelector:@selector(ping:didFailToSendPingWithSummary:error:)]) {
[self.delegate ping:self didFailToSendPingWithSummary:pingSummaryCopyAfterFailure error:[NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil]];
}
});
}
}
}
-(void)stop {
@synchronized(self) {
if (!self.isStopped) {
self.isPinging = NO;
self.isReady = NO;
//destroy listenThread by closing socket (listenThread)
if (self.socket) {
close(self.socket);
self.socket = 0;
}
//destroy host
self.hostAddress = nil;
//clean up pendingpings
[self.pendingPings removeAllObjects];
self.pendingPings = nil;
for (NSNumber *key in [self.timeoutTimers copy]) {
NSTimer *timer = self.timeoutTimers[key];
[timer invalidate];
}
//clean up timeouttimers
[self.timeoutTimers removeAllObjects];
self.timeoutTimers = nil;
//reset seq number
self.nextSequenceNumber = 0;
self.isStopped = YES;
}
}
}
#pragma mark - util
static uint16_t in_cksum(const void *buffer, size_t bufferLen)
// This is the standard BSD checksum code, modified to use modern types.
{
size_t bytesLeft;
int32_t sum;
const uint16_t * cursor;
union {
uint16_t us;
uint8_t uc[2];
} last;
uint16_t answer;
bytesLeft = bufferLen;
sum = 0;
cursor = buffer;
/*
* Our algorithm is simple, using a 32 bit accumulator (sum), we add
* sequential 16 bit words to it, and at the end, fold back all the
* carry bits from the top 16 bits into the lower 16 bits.
*/
while (bytesLeft > 1) {
sum += *cursor;
cursor += 1;
bytesLeft -= 2;
}
/* mop up an odd byte, if necessary */
if (bytesLeft == 1) {
last.uc[0] = * (const uint8_t *) cursor;
last.uc[1] = 0;
sum += last.us;
}
/* add back carry outs from top 16 bits to low 16 bits */
sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */
sum += (sum >> 16); /* add carry */
answer = (uint16_t) ~sum; /* truncate to 16 bits */
return answer;
}
+(NSString *)sourceAddressInPacket:(NSData *)packet {
// Returns the source address of the IP packet
const struct IPHeader *ipPtr;
const uint8_t *sourceAddress;
if ([packet length] >= sizeof(IPHeader)) {
ipPtr = (const IPHeader *)[packet bytes];
sourceAddress = ipPtr->sourceAddress;//dont need to swap byte order those cuz theyre the smallest atomic unit (1 byte)
NSString *ipString = [NSString stringWithFormat:@"%d.%d.%d.%d", sourceAddress[0], sourceAddress[1], sourceAddress[2], sourceAddress[3]];
return ipString;
}
else return nil;
}
+ (NSUInteger)icmp4HeaderOffsetInPacket:(NSData *)packet
// Returns the offset of the ICMPHeader within an IP packet.
{
NSUInteger result;
const struct IPHeader * ipPtr;
size_t ipHeaderLength;
result = NSNotFound;
if ([packet length] >= (sizeof(IPHeader) + sizeof(ICMPHeader))) {
ipPtr = (const IPHeader *) [packet bytes];
assert((ipPtr->versionAndHeaderLength & 0xF0) == 0x40); // IPv4
assert(ipPtr->protocol == 1); // ICMP
ipHeaderLength = (ipPtr->versionAndHeaderLength & 0x0F) * sizeof(uint32_t);
if ([packet length] >= (ipHeaderLength + sizeof(ICMPHeader))) {
result = ipHeaderLength;
}
}
return result;
}
+ (const struct ICMPHeader *)icmp4InPacket:(NSData *)packet
// See comment in header.
{
const struct ICMPHeader * result;
NSUInteger icmpHeaderOffset;
result = nil;
icmpHeaderOffset = [self icmp4HeaderOffsetInPacket:packet];
if (icmpHeaderOffset != NSNotFound) {
result = (const struct ICMPHeader *) (((const uint8_t *)[packet bytes]) + icmpHeaderOffset);
}
return result;
}
- (BOOL)isValidPingResponsePacket:(NSMutableData *)packet
{
BOOL result;
switch (self.hostAddressFamily) {
case AF_INET: {
result = [self isValidPing4ResponsePacket:packet];
} break;
case AF_INET6: {
result = [self isValidPing6ResponsePacket:packet];
} break;
default: {
result = NO;
} break;
}
return result;
}
- (BOOL)isValidPing4ResponsePacket:(NSMutableData *)packet
// Returns true if the packet looks like a valid ping response packet destined
// for us.
{
BOOL result;
NSUInteger icmpHeaderOffset;
ICMPHeader * icmpPtr;
uint16_t receivedChecksum;
uint16_t calculatedChecksum;
result = NO;
icmpHeaderOffset = [[self class] icmp4HeaderOffsetInPacket:packet];
if (icmpHeaderOffset != NSNotFound) {
icmpPtr = (struct ICMPHeader *) (((uint8_t *)[packet mutableBytes]) + icmpHeaderOffset);
receivedChecksum = icmpPtr->checksum;
icmpPtr->checksum = 0;
calculatedChecksum = in_cksum(icmpPtr, [packet length] - icmpHeaderOffset);
icmpPtr->checksum = receivedChecksum;
if (receivedChecksum == calculatedChecksum) {
if ( (icmpPtr->type == kICMPv4TypeEchoReply) && (icmpPtr->code == 0) ) {
if ( OSSwapBigToHostInt16(icmpPtr->identifier) == self.identifier ) {
if ( OSSwapBigToHostInt16(icmpPtr->sequenceNumber) < self.nextSequenceNumber ) {
result = YES;
}
}
}
}
}
// NSLog(@"valid: %@, type: %d", _b(result), icmpPtr->type);
return result;
}
- (BOOL)isValidPing6ResponsePacket:(NSMutableData *)packet
// Returns true if the IPv6 packet looks like a valid ping response packet destined
// for us.
{
BOOL result;
const ICMPHeader * icmpPtr;
result = NO;
if (packet.length >= sizeof(*icmpPtr)) {
icmpPtr = packet.bytes;
if ( (icmpPtr->type == kICMPv6TypeEchoReply) && (icmpPtr->code == 0) ) {
if ( OSSwapBigToHostInt16(icmpPtr->identifier) == self.identifier ) {
if ( OSSwapBigToHostInt16(icmpPtr->sequenceNumber) < self.nextSequenceNumber ) {
result = YES;
}
}
}
}
return result;
}
-(NSData *)generateDataWithLength:(NSUInteger)length {
//create a buffer full of 7's of specified length
char tempBuffer[length];
memset(tempBuffer, 7, length);
return [[NSData alloc] initWithBytes:tempBuffer length:length];
}
- (void)_invokeTimeoutCallback:(NSTimer *)timer
{
dispatch_block_t callback = timer.userInfo;
if (callback) {
callback();
}
}
- (NSData *)pingPacketWithType:(uint8_t)type payload:(NSData *)payload requiresChecksum:(BOOL)requiresChecksum {
NSMutableData * packet;
ICMPHeader * icmpPtr;
packet = [NSMutableData dataWithLength:sizeof(*icmpPtr) + payload.length];
if (packet == nil) { return nil; }
icmpPtr = packet.mutableBytes;
icmpPtr->type = type;
icmpPtr->code = 0;
icmpPtr->checksum = 0;
icmpPtr->identifier = OSSwapHostToBigInt16(self.identifier);
icmpPtr->sequenceNumber = OSSwapHostToBigInt16(self.nextSequenceNumber);
memcpy(&icmpPtr[1], [payload bytes], [payload length]);
if (requiresChecksum) {
// The IP checksum routine returns a 16-bit number that's already in correct byte order
// (due to wacky 1's complement maths), so we just put it into the packet as a 16-bit unit.
icmpPtr->checksum = in_cksum(packet.bytes, packet.length);
}
return packet;
}
- (sa_family_t)hostAddressFamily {
sa_family_t result = AF_UNSPEC;
// Save a reference to a local variable, avoid crash when hostAddress is release by other thread.
NSData *hostAddress = self.hostAddress;
if (hostAddress != nil && hostAddress.length >= sizeof(struct sockaddr)) {
result = ((const struct sockaddr *)hostAddress.bytes)->sa_family;
}
return result;
}
- (NSString*)ntop:(struct sockaddr *)sa len:(socklen_t)len {
char ntop[NI_MAXHOST] = { 0 };
int ecode = getnameinfo(sa, len, ntop, sizeof(ntop), NULL, 0, NI_NUMERICHOST);
if (ecode == 0) {
return [[NSString alloc] initWithUTF8String:ntop];
} else {
return nil;
}
}
#pragma mark - memory
-(id)init {
if (self = [super init]) {
self.setupQueue = dispatch_queue_create("GBPing setup queue", 0);
self.isStopped = YES;
self.identifier = arc4random();
self.useIpv4 = YES;
self.useIpv6 = YES;
}
return self;
}
-(void)dealloc {
self.delegate = nil;
self.host = nil;
@synchronized (self) {
self.timeoutTimers = nil;
self.pendingPings = nil;
}
self.hostAddress = nil;
//clean up socket to be sure
if (self.socket) {
close(self.socket);
self.socket = 0;
}
}
@end
================================================
FILE: ios/ProxyPin/vpn/ping/GBPingHelper.swift
================================================
//
// GBPingHelper.swift
//
import Foundation
public typealias Handler = ((_ response: [String: Any]) -> Void)
public class GBPingHelper: NSObject {
private var ping: GBPing?
private let delegate = PingDelegate()
func start(withHost host: String, ipv4: Bool, ipv6: Bool, count: UInt, interval: TimeInterval, timeout: TimeInterval, ttl: UInt, handler: @escaping Handler) {
ping?.stop()
ping = GBPing()
guard let ping = ping else {
return
}
ping.host = host
ping.useIpv4 = ipv4
ping.useIpv6 = ipv6
ping.count = count
ping.pingPeriod = interval
ping.timeout = timeout
if ttl > 0 {
ping.ttl = ttl
}
delegate.handler = handler
ping.delegate = delegate
ping.setup { success, err in
if let err = err as NSError? {
if err.domain == kCFErrorDomainCFNetwork as String {
handler(["error": "UnknownHost"])
} else {
handler(["error": "UnknownError"])
}
return
}
if success {
self.delegate.transmitted = 0
self.delegate.received = 0
ping.startPinging()
}
}
}
func stop() {
ping?.stop()
}
}
private class PingDelegate: NSObject, GBPingDelegate {
public var handler: Handler?
public var transmitted = 0
public var received = 0
func handle(_ summary: GBPingSummary, error: String? = nil) {
guard let handler = handler else {
return
}
var ret: [String: Any] = [:]
ret["seq"] = summary.sequenceNumber
ret["host"] = summary.host
ret["ip"] = summary.ip
ret["ttl"] = summary.ttl
ret["time"] = summary.rtt
ret["error"] = error
handler(ret)
}
func ping(_ pinger: GBPing, didSendPingWith summary: GBPingSummary) {
transmitted += 1
}
func ping(_ pinger: GBPing, didTimeoutWith summary: GBPingSummary) {
handle(summary, error: "RequestTimedOut")
}
func ping(_ pinger: GBPing, didReceiveReplyWith summary: GBPingSummary) {
received += 1
handle(summary)
}
func ping(_ pinger: GBPing, didFinishWithTime time: TimeInterval) {
guard let handler = handler else {
return
}
var ret: [String: Any] = [:]
ret["time"] = time
ret["received"] = received
ret["transmitted"] = transmitted
handler(ret)
}
}
================================================
FILE: ios/ProxyPin/vpn/ping/GBPingSummary.h
================================================
//
// GBPingSummary.h
// GBPing
//
// Created by Luka Mirosevic on 05/11/2012.
// Copyright (c) 2012 Goonbee. All rights reserved.
//
#import
@interface GBPingSummary : NSObject
typedef enum {
GBPingStatusPending,
GBPingStatusSuccess,
GBPingStatusFail,
} GBPingStatus;
@property (assign, nonatomic) NSUInteger sequenceNumber;
@property (assign, nonatomic) NSUInteger payloadSize;
@property (assign, nonatomic) NSUInteger ttl;
@property (strong, nonatomic, nullable) NSString *host;
@property (strong, nonatomic, nullable) NSString *ip;
@property (strong, nonatomic, nullable) NSDate *sendDate;
@property (strong, nonatomic, nullable) NSDate *receiveDate;
@property (assign, nonatomic) NSTimeInterval rtt;
@property (assign, nonatomic) GBPingStatus status;
@end
================================================
FILE: ios/ProxyPin/vpn/ping/GBPingSummary.m
================================================
//
// GBPingSummary.m
// GBPing
//
// Created by Luka Mirosevic on 05/11/2012.
// Copyright (c) 2012 Goonbee. All rights reserved.
//
#import "GBPingSummary.h"
@implementation GBPingSummary
#pragma mark - custom acc
-(void)setHost:(NSString *)host {
_host = host;
}
-(NSTimeInterval)rtt {
if (self.sendDate) {
return [self.receiveDate timeIntervalSinceDate:self.sendDate];
}
else {
return 0;
}
}
#pragma mark - copying
-(id)copyWithZone:(NSZone *)zone {
GBPingSummary *copy = [[[self class] allocWithZone:zone] init];
copy.sequenceNumber = self.sequenceNumber;
copy.payloadSize = self.payloadSize;
copy.ttl = self.ttl;
copy.host = [self.host copy];
copy.ip = [self.ip copy];
copy.sendDate = [self.sendDate copy];
copy.receiveDate = [self.receiveDate copy];
copy.status = self.status;
return copy;
}
#pragma mark - memory
-(id)init {
if (self = [super init]) {
self.status = GBPingStatusPending;
}
return self;
}
-(void)dealloc {
self.host = nil;
self.ip = nil;
self.sendDate = nil;
self.receiveDate = nil;
}
#pragma mark - description
-(NSString *)description {
return [NSString stringWithFormat:@"host: %@, ip:%@, seq: %lu, status: %d, ttl: %lu, payloadSize: %lu, sendDate: %@, receiveDate: %@, rtt: %f", self.host, self.ip, (unsigned long)self.sequenceNumber, self.status, (unsigned long)self.ttl, (unsigned long)self.payloadSize, self.sendDate, self.receiveDate, self.rtt];
}
@end
================================================
FILE: ios/ProxyPin/vpn/ping/ICMPHeader.h
================================================
//
// ICMPHeader.h
// GBPing
//
// Created by Luka Mirosevic on 15/11/2012.
// Copyright (c) 2012 Goonbee. All rights reserved.
//
#ifndef GBPing_ICMPHeader_h
#define GBPing_ICMPHeader_h
#include
#pragma mark - IP and ICMP On-The-Wire Format
// The following declarations specify the structure of ping packets on the wire.
// IP header structure:
struct IPHeader {
uint8_t versionAndHeaderLength;
uint8_t differentiatedServices;
uint16_t totalLength;
uint16_t identification;
uint16_t flagsAndFragmentOffset;
uint8_t timeToLive;
uint8_t protocol;
uint16_t headerChecksum;
uint8_t sourceAddress[4];
uint8_t destinationAddress[4];
// options...
// data...
};
typedef struct IPHeader IPHeader;
__Check_Compile_Time(sizeof(IPHeader) == 20);
__Check_Compile_Time(offsetof(IPHeader, versionAndHeaderLength) == 0);
__Check_Compile_Time(offsetof(IPHeader, differentiatedServices) == 1);
__Check_Compile_Time(offsetof(IPHeader, totalLength) == 2);
__Check_Compile_Time(offsetof(IPHeader, identification) == 4);
__Check_Compile_Time(offsetof(IPHeader, flagsAndFragmentOffset) == 6);
__Check_Compile_Time(offsetof(IPHeader, timeToLive) == 8);
__Check_Compile_Time(offsetof(IPHeader, protocol) == 9);
__Check_Compile_Time(offsetof(IPHeader, headerChecksum) == 10);
__Check_Compile_Time(offsetof(IPHeader, sourceAddress) == 12);
__Check_Compile_Time(offsetof(IPHeader, destinationAddress) == 16);
// ICMP type and code combinations:
enum {
kICMPv4TypeEchoRequest = 8,
kICMPv4TypeEchoReply = 0
};
enum {
kICMPv6TypeEchoRequest = 128,
kICMPv6TypeEchoReply = 129
};
// ICMP header structure:
struct ICMPHeader {
uint8_t type;
uint8_t code;
uint16_t checksum;
uint16_t identifier;
uint16_t sequenceNumber;
// data...
};
typedef struct ICMPHeader ICMPHeader;
__Check_Compile_Time(sizeof(ICMPHeader) == 8);
__Check_Compile_Time(offsetof(ICMPHeader, type) == 0);
__Check_Compile_Time(offsetof(ICMPHeader, code) == 1);
__Check_Compile_Time(offsetof(ICMPHeader, checksum) == 2);
__Check_Compile_Time(offsetof(ICMPHeader, identifier) == 4);
__Check_Compile_Time(offsetof(ICMPHeader, sequenceNumber) == 6);
#endif
================================================
FILE: ios/ProxyPin/vpn/socket/ClientPacketWriter.swift
================================================
//
// ClientPacketWriter.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/
import Foundation
import NetworkExtension
class ClientPacketWriter: NSObject {
private var packetFlow: NEPacketTunnelFlow
private var isShutdown = false
init(packetFlow: NEPacketTunnelFlow) {
self.packetFlow = packetFlow
}
func write(data: Data) {
self.packetFlow.writePackets([data], withProtocols: [NSNumber(value: AF_INET)])
}
func shutdown() {
self.isShutdown = true
}
}
================================================
FILE: ios/ProxyPin/vpn/socket/CloseableConnection.swift
================================================
//
// CloseableConnection.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/17.
//
import Foundation
protocol CloseableConnection {
/// Closes the connection
func closeConnection(connection: Connection)
}
================================================
FILE: ios/ProxyPin/vpn/socket/SocketIOService.swift
================================================
//
// ProxySocketIOService.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/17.
//
import Foundation
import NetworkExtension
import os.log
class SocketIOService {
// private static let maxReceiveBufferSize = 16384
private static let maxReceiveBufferSize = 1480
private let queue: DispatchQueue = DispatchQueue(label: "ProxyPin.SocketIOService", attributes: .concurrent)
private var clientPacketWriter: NEPacketTunnelFlow
private var shutdown = false
init(clientPacketWriter: NEPacketTunnelFlow) {
self.clientPacketWriter = clientPacketWriter
}
public func stop() {
os_log("Stopping SocketIOService", log: OSLog.default, type: .default)
queue.async(flags: .barrier) {
self.shutdown = true
}
// queue.suspend()
}
//从connection接受数据 写到client
public func registerSession(connection: Connection) {
connection.channel!.stateUpdateHandler = { state in
// os_log("Connection %{public}@ state changed to %{public}@", log: OSLog.default, type: .default, connection.description, String(describing: state))
switch state {
case .ready:
connection.isConnected = true
os_log("Connected to %{public}@ on receiveMessage", log: OSLog.default, type: .default, connection.description)
//接受远程服务器的数据
connection.sendToDestination()
self.receiveMessage(connection: connection)
case .cancelled:
connection.isConnected = false
os_log("Connection cancelled %{public}@", log: OSLog.default, type: .default, connection.description)
connection.closeConnection()
self.sendFin(connection: connection)
case .failed(let error):
connection.isConnected = false
os_log("Failed to connect: %{public}@ %{public}@", log: OSLog.default, type: .error,connection.description, error.localizedDescription)
connection.closeConnection()
default:
os_log("Connection %{public}@ entered unhandled state: %{public}@", log: OSLog.default, type: .default, connection.description, String(describing: state))
break
}
}
connection.channel!.start(queue: self.queue)
}
private func receiveMessage(connection: Connection) {
if (shutdown) {
os_log("SocketIOService is shutting down", log: OSLog.default, type: .default)
return
}
if (connection.nwProtocol == .UDP) {
readUDP(connection: connection)
} else {
readTCP(connection: connection)
}
if (connection.isAbortingConnection) {
os_log("Connection is aborting", log: OSLog.default, type: .default)
connection.closeConnection()
return
}
}
func readTCP(connection: Connection) {
// os_log("Reading from TCP socket")
if connection.isAbortingConnection {
os_log("Connection is aborting", log: OSLog.default, type: .default)
return
}
guard let channel = connection.channel else {
os_log("Invalid channel type", log: OSLog.default, type: .error)
return
}
channel.receive(minimumIncompleteLength: 1, maximumLength: Self.maxReceiveBufferSize) { (data, context, isComplete, error) in
self.queue.async(flags: .barrier) {
// os_log("[SocketIOService] Received TCP data packet %{public}@ length %d", log: OSLog.default, type: .default, connection.description, data?.count ?? -1)
if let error = error {
os_log("Failed to read from TCP socket: %@", log: OSLog.default, type: .error, error as CVarArg)
connection.isAbortingConnection = true
return
}
self.pushDataToClient(buffer: data ?? Data() , connection: connection)
// Recursively call readTCP to continue reading messages
self.receiveMessage(connection: connection)
if (isComplete) {
connection.isAbortingConnection = true
return
}
}
}
}
func synchronized(_ lock: AnyObject, closure: () -> Void) {
// objc_sync_enter(lock)
closure()
// objc_sync_exit(lock)
}
///create packet data and send it to VPN client
private func pushDataToClient(buffer: Data, connection: Connection) {
// Last piece of data is usually smaller than MAX_RECEIVE_BUFFER_SIZE. We use this as a
// trigger to set PSH on the resulting TCP packet that goes to the VPN.
connection.hasReceivedLastSegment = buffer.count <= 0
guard let ipHeader = connection.lastIpHeader, let tcpHeader = connection.lastTcpHeader else {
os_log("Invalid ipHeader or tcpHeader", log: OSLog.default, type: .error)
return
}
synchronized(connection) {
let unAck = connection.sendNext
//处理益处问题
let nextUnAck = UInt32(truncatingIfNeeded: (connection.sendNext + UInt32(buffer.count)) % UInt32.max)
connection.sendNext = nextUnAck
let data = TCPPacketFactory.createResponsePacketData(
ipHeader: ipHeader,
tcpHeader: tcpHeader,
packetData: buffer,
isPsh: connection.hasReceivedLastSegment,
ackNumber: connection.recSequence,
seqNumber: unAck,
timeSender: connection.timestampSender,
timeReplyTo: connection.timestampReplyTo
)
self.clientPacketWriter.writePackets([data], withProtocols: [NSNumber(value: AF_INET)])
// os_log("[SocketIOService] Sent TCP data packet to client %{public}@ length:%d seq:%u ack:%u", log: OSLog.default, type: .default, connection.description, buffer.count, unAck, connection.recSequence)
}
}
private func sendFin(connection: Connection) {
if (connection.nwProtocol != .TCP) {
return
}
guard let ipHeader = connection.lastIpHeader, let tcpHeader = connection.lastTcpHeader else {
os_log("Invalid ipHeader or tcpHeader", log: OSLog.default, type: .error)
return
}
synchronized(connection) {
let data = TCPPacketFactory.createFinData(
ipHeader: ipHeader,
tcpHeader: tcpHeader,
ackNumber: connection.recSequence,
seqNumber: connection.sendNext,
timeSender: connection.timestampSender,
timeReplyTo: connection.timestampReplyTo
)
self.clientPacketWriter.writePackets([data], withProtocols: [NSNumber(value: AF_INET)])
}
}
func readUDP(connection: Connection) {
guard let channel = connection.channel else {
os_log("Invalid channel type", log: OSLog.default, type: .error)
return
}
channel.receive(minimumIncompleteLength: 1, maximumLength: 65507) { (data, context, isComplete, error) in
self.queue.async(flags: .barrier) {
if let error = error {
os_log("Failed to read from UDP socket: %@", log: OSLog.default, type: .error, error as CVarArg)
connection.isAbortingConnection = true
return
}
// os_log("Received UDP data packet length %d", log: OSLog.default, type: .debug, data?.count ?? 0)
guard let data = data, !data.isEmpty else {
return
}
guard let ipHeader = connection.lastIpHeader, let udpHeader = connection.lastUdpHeader else {
os_log("Missing IP or UDP header for connection %{public}@", log: OSLog.default, type: .error, connection.description)
return
}
let packetData = UDPPacketFactory.createResponsePacket(
ip: ipHeader,
udp: udpHeader,
packetData: data
)
self.clientPacketWriter.writePackets([packetData], withProtocols: [NSNumber(value: AF_INET)])
// Recursively call receiveMessage to continue receiving messages
self.receiveMessage(connection: connection)
}
}
}
}
================================================
FILE: ios/ProxyPin/vpn/transport/Packet.swift
================================================
//
// Packet.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/17.
//
import Foundation
class Packet {
var ipHeader: IP4Header
var transportHeader: TransportHeader
var buffer: Data
init(ipHeader: IP4Header, transportHeader: TransportHeader, buffer: Data) {
self.ipHeader = ipHeader
self.transportHeader = transportHeader
self.buffer = buffer
}
}
================================================
FILE: ios/ProxyPin/vpn/transport/protocol/ICMPPacket.swift
================================================
//
// ICMPPacket.swift
// ProxyPin
//
// Created by wanghongen on 2024/10/3.
//
import Foundation
class ICMPPacket {
// Two ICMP packets we can handle: simple ping & pong
static let ECHO_REQUEST_TYPE: UInt8 = 8
static let ECHO_SUCCESS_TYPE: UInt8 = 0
// One very common packet we ignore: connection rejection. Unclear why this happens,
// random incoming connections that the phone tries to reply to? Nothing we can do though,
// as we can't forward ICMP onwards, and we can't usefully respond or react.
static let DESTINATION_UNREACHABLE_TYPE: UInt8 = 3
let type: UInt8
let code: UInt8 // 0 for request, 0 for success, 0 - 15 for error subtypes
let checksum: UInt16
let identifier: UInt16
let sequenceNumber: UInt16
let data: [UInt8]
init(type: UInt8, code: UInt8, checksum: UInt16, identifier: UInt16, sequenceNumber: UInt16, data: [UInt8]) {
self.type = type
self.code = code
self.checksum = checksum
self.identifier = identifier
self.sequenceNumber = sequenceNumber
self.data = data
}
var description: String {
return "ICMP packet type \(type)/\(code) id:\(identifier) seq:\(sequenceNumber) and \(data.count) bytes of data"
}
}
class ICMPPacketFactory {
static func parseICMPPacket(_ stream: inout Data) -> ICMPPacket? {
guard stream.count >= 8 else { return nil }
let type = stream.removeFirst()
let code = stream.removeFirst()
let checksum = stream.withUnsafeBytes { $0.load(as: UInt16.self) }
stream.removeFirst(2)
let identifier = stream.withUnsafeBytes { $0.load(as: UInt16.self) }
stream.removeFirst(2)
let sequenceNumber = stream.withUnsafeBytes { $0.load(as: UInt16.self) }
stream.removeFirst(2)
let data = Array(stream)
return ICMPPacket(type: type, code: code, checksum: checksum, identifier: identifier, sequenceNumber: sequenceNumber, data: data)
}
static func buildSuccessPacket(_ requestPacket: ICMPPacket) -> ICMPPacket {
return ICMPPacket(
type: ICMPPacket.ECHO_SUCCESS_TYPE,
code: 0,
checksum: 0,
identifier: requestPacket.identifier,
sequenceNumber: requestPacket.sequenceNumber,
data: requestPacket.data
)
}
static func packetToBuffer(ipHeader: IP4Header, packet: ICMPPacket) -> Data {
var buffer = Data()
buffer.append(ipHeader.toBytes())
var icmpDataBuffer = Data()
icmpDataBuffer.append(packet.type)
icmpDataBuffer.append(packet.code)
icmpDataBuffer.append(contentsOf: withUnsafeBytes(of: UInt16(0), Array.init))
if packet.type == ICMPPacket.ECHO_REQUEST_TYPE || packet.type == ICMPPacket.ECHO_SUCCESS_TYPE {
icmpDataBuffer.append(contentsOf: packet.identifier.bytes)
icmpDataBuffer.append(contentsOf: packet.sequenceNumber.bytes)
icmpDataBuffer.append(contentsOf: packet.data)
} else {
fatalError("Can't serialize unrecognized ICMP packet type")
}
let checksum = PacketUtil.calculateChecksum(data: icmpDataBuffer, offset: 0, length: icmpDataBuffer.count)
icmpDataBuffer.replaceSubrange(2..<4, with: checksum)
buffer.append(icmpDataBuffer)
return buffer
}
}
================================================
FILE: ios/ProxyPin/vpn/transport/protocol/IP4Header.swift
================================================
//
// IP4Header.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/16.
//
import Foundation
import os.log
// IPv4 header data structure
class IP4Header {
var ipVersion: UInt8 // 对于IPv4,其值为4(因此命名为IPv4)。 4bit
var internetHeaderLength: UInt8 // 头部长度 4bit
var diffTypeOfService: UInt8 // 差分服务代码点 =>6位
var ecn: UInt8 // 显式拥塞通知(ECN)
var totalLength: UInt16 // 此IP数据包的总长度 16bit
var identification: UInt16 // 主要用于唯一标识单个IP数据报的片段组。 16bit
var mayFragment: Bool // 用于指示数据报是否可以分段。 1bit
var lastFragment: Bool // 用于指示数据报是否是片段中的最后一个。 1bit
var fragmentOffset: UInt16 // 指定特定片段相对于原始未分段的IP数据报的开始的偏移量。 13bit
var timeToLive: UInt8 // 用于防止数据报持续存在。8bit
var protocolNumber: UInt8 // 定义IP数据报的数据部分中使用的协议。 8bit
var headerChecksum: UInt16 // 用于对头部进行错误检查的16位字段。 16bit
var sourceIP: UInt32 // 发送者的IPv4地址。 32bit
var destinationIP: UInt32 // 接收者的IPv4地址。 32bit
//用于控制或识别片段的3比特字段。
//bit 0: 保留;必须为零
//bit 1: Don't Fragment (DF)
//bit 2: More Fragments (MF)
private var flag: UInt8
init(
ipVersion: UInt8, internetHeaderLength: UInt8, diffTypeOfService: UInt8, ecn: UInt8, totalLength: UInt16, identification: UInt16,
mayFragment: Bool, lastFragment: Bool, fragmentOffset: UInt16, timeToLive: UInt8, protocolNumber: UInt8, headerChecksum: UInt16,
sourceIP: UInt32, destinationIP: UInt32
) {
self.ipVersion = ipVersion
self.internetHeaderLength = internetHeaderLength
self.diffTypeOfService = diffTypeOfService
self.ecn = ecn
self.totalLength = totalLength
self.identification = identification
self.mayFragment = mayFragment
self.lastFragment = lastFragment
self.fragmentOffset = fragmentOffset
self.timeToLive = timeToLive
self.protocolNumber = protocolNumber
self.headerChecksum = headerChecksum
self.sourceIP = sourceIP
self.destinationIP = destinationIP
self.flag = IP4Header.initFlag(mayFragment: mayFragment, lastFragment: lastFragment)
}
private static func initFlag(mayFragment: Bool, lastFragment: Bool) -> UInt8 {
var initFlag: UInt8 = 0
if mayFragment {
initFlag = 0x40
}
if lastFragment {
initFlag |= 0x20
}
return initFlag
}
func setMayFragment(_ mayFragment: Bool) {
self.mayFragment = mayFragment
flag = mayFragment ? (flag | 0x40) : (flag & 0xBF)
}
func getIPHeaderLength() -> Int {
return Int(internetHeaderLength * 4)
}
func copy() -> IP4Header {
return IP4Header(
ipVersion: ipVersion, internetHeaderLength: internetHeaderLength, diffTypeOfService: diffTypeOfService, ecn: ecn, totalLength: totalLength, identification: identification,
mayFragment: mayFragment, lastFragment: lastFragment, fragmentOffset: fragmentOffset, timeToLive: timeToLive, protocolNumber: protocolNumber, headerChecksum: headerChecksum,
sourceIP: sourceIP, destinationIP: destinationIP
)
}
func toBytes() -> Data {
var buffer = Data()
buffer.append(UInt8((ipVersion << 4) + internetHeaderLength))
buffer.append(UInt8((diffTypeOfService << 2) + ecn))
buffer.append(contentsOf: totalLength.bytes)
buffer.append(contentsOf: identification.bytes)
//组合标志和部分片段偏移
buffer.append(UInt8((fragmentOffset >> 8) & 0x1F) | flag)
buffer.append(UInt8(fragmentOffset & 0xFF))
buffer.append(timeToLive)
buffer.append(protocolNumber)
buffer.append(contentsOf: headerChecksum.bytes)
buffer.append(contentsOf: sourceIP.bytes)
buffer.append(contentsOf: destinationIP.bytes)
return buffer
}
}
class IPPacketFactory {
static let IP4_HEADER_SIZE = 20
static let IP4_VERSION: UInt8 = 0x04
//从给定的ByteBuffer流创建IPv4标头
static func createIP4Header(data: Data) -> IP4Header? {
guard data.count >= IP4_HEADER_SIZE else {
return nil
}
let buffer = [UInt8](data)
let versionAndHeaderLength = buffer[0]
let ipVersion = versionAndHeaderLength >> 4
guard ipVersion == IP4_VERSION else {
return nil
}
let internetHeaderLength = versionAndHeaderLength & 0x0F
let typeOfService = buffer[1]
let diffTypeOfService = typeOfService >> 2
let ecn = typeOfService & 0x03
let totalLength = UInt16(buffer[2]) << 8 | UInt16(buffer[3])
let identification = UInt16(buffer[4]) << 8 | UInt16(buffer[5])
let flagsAndFragmentOffset = UInt16(buffer[6]) << 8 | UInt16(buffer[7])
let mayFragment = (flagsAndFragmentOffset & 0x4000) != 0
let lastFragment = (flagsAndFragmentOffset & 0x2000) != 0
let fragmentOffset = flagsAndFragmentOffset & 0x1FFF
let timeToLive = buffer[8]
let protocolNumber = buffer[9]
let checksum = UInt16(buffer[10]) << 8 | UInt16(buffer[11])
let sourceIp = UInt32(buffer[12]) << 24 | UInt32(buffer[13]) << 16 | UInt32(buffer[14]) << 8 | UInt32(buffer[15])
let desIp = UInt32(buffer[16]) << 24 | UInt32(buffer[17]) << 16 | UInt32(buffer[18]) << 8 | UInt32(buffer[19])
if internetHeaderLength > 5 {
// drop the IP option
for _ in 0..<(internetHeaderLength - 5) {
// Skip the IP options
}
}
return IP4Header(
ipVersion: ipVersion, internetHeaderLength: internetHeaderLength, diffTypeOfService: diffTypeOfService, ecn: ecn, totalLength: totalLength, identification: identification,
mayFragment: mayFragment, lastFragment: lastFragment, fragmentOffset: fragmentOffset, timeToLive: timeToLive, protocolNumber: protocolNumber, headerChecksum: checksum,
sourceIP: sourceIp, destinationIP: desIp
)
}
public static func printPacket(data: Data) {
guard let ipHeader = createIP4Header(data: data) else {
return
}
os_log("IP Header: version: %{public}d, internetHeaderLength: %{public}d, diffTypeOfService: %{public}d, ecn: %{public}d, totalLength: %{public}d, identification: %{public}d, mayFragment: %{public}d, lastFragment: %{public}d, fragmentOffset: %{public}d, timeToLive: %{public}d, protocolNumber: %{public}d, headerChecksum: %{public}d, sourceIP: %{public}@, destinationIP: %{public}@", log: OSLog.default, type: .default, ipHeader.ipVersion, ipHeader.internetHeaderLength, ipHeader.diffTypeOfService, ipHeader.ecn, ipHeader.totalLength, ipHeader.identification, ipHeader.mayFragment, ipHeader.lastFragment, ipHeader.fragmentOffset, ipHeader.timeToLive, ipHeader.protocolNumber, ipHeader.headerChecksum, PacketUtil.intToIPAddress(ipHeader.sourceIP), PacketUtil.intToIPAddress(ipHeader.destinationIP))
}
}
================================================
FILE: ios/ProxyPin/vpn/transport/protocol/TCPHeader.swift
================================================
//
// TCPHeader.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/16.
//
import Foundation
/// Represents a TCP header in a network packet.
class TCPHeader : TransportHeader{
/// Source port number (16 bits)
var sourcePort: UInt16
/// Destination port number (16 bits)
var destinationPort: UInt16
/// Sequence number (32 bits)
var sequenceNumber: UInt32
/// Acknowledgment number (32 bits)
var ackNumber: UInt32
/// Data offset (4 bits)
var dataOffset: UInt8
var isNS: Bool = false // ECN-nonce concealment protection (experimental: see RFC 3540)
/// Flags (9 bits)
var flags: UInt8
/// Window size (16 bits)
var windowSize: UInt16
/// Checksum (16 bits)
var checksum: UInt16
/// Urgent pointer (16 bits)
var urgentPointer: UInt16
/// Options (variable length)
var options: Data?
var payload: Data?
//Static section for constants
static let END_OF_OPTIONS_LIST: UInt8 = 0
static let NO_OPERATION: UInt8 = 1
static let MAX_SEGMENT_SIZE: UInt8 = 2
static let WINDOW_SCALE: UInt8 = 3
static let SELECTIVE_ACK_PERMITTED: UInt8 = 4
static let TIME_STAMP: UInt8 = 8
init(sourcePort: UInt16, destinationPort: UInt16, sequenceNumber: UInt32, ackNumber: UInt32, dataOffset: UInt8, isNS: Bool, flags: UInt8, windowSize: UInt16, checksum: UInt16, urgentPointer: UInt16, options: Data?, payload: Data? = nil) {
self.sourcePort = sourcePort
self.destinationPort = destinationPort
self.sequenceNumber = sequenceNumber
self.ackNumber = ackNumber
self.dataOffset = dataOffset
self.isNS = isNS
self.flags = flags
self.windowSize = windowSize
self.checksum = checksum
self.urgentPointer = urgentPointer
self.options = options
self.payload = payload
}
//options
var maxSegmentSize: UInt16 = 0
private var windowScale: UInt8 = 0
private var isSelectiveAckPermitted = false
var timeStampSender = 0
var timeStampReplyTo = 0
func getSourcePort() -> Int {
return Int(sourcePort)
}
func getDestinationPort() -> Int {
return Int(destinationPort)
}
func isFIN() -> Bool {
return flags & 0x01 != 0
}
/// Checks if the SYN flag is set.
func isSYN() -> Bool {
return flags & 0x02 != 0
}
/// Checks if the RST flag is set.
func isRST() -> Bool {
return flags & 0x04 != 0
}
/// Checks if the PSH flag is set.
func isPSH() -> Bool {
return flags & 0x08 != 0
}
/// Checks if the ACK flag is set.
func isACK() -> Bool {
return flags & 0x10 != 0
}
/// Checks if the URG flag is set.
func isURG() -> Bool {
return flags & 0x20 != 0
}
/// Checks if the ECE flag is set.
func isECE() -> Bool {
return flags & 0x40 != 0
}
/// Checks if the CWR flag is set.
func isCWR() -> Bool {
return flags & 0x80 != 0
}
/// Sets or clears the RST flag.
func setIsRST(_ isRST: Bool) {
flags = isRST ? (flags | 0x04) : (flags & 0xFB)
}
/// Sets or clears the SYN flag.
func setIsSYN(_ isSYN: Bool) {
flags = isSYN ? (flags | 0x02) : (flags & 0xFD)
}
/// Sets or clears the FIN flag.
func setIsFIN(_ isFIN: Bool) {
flags = isFIN ? (flags | 0x01) : (flags & 0xFE)
}
/// Sets or clears the PSH flag.
func setIsPSH(_ isPSH: Bool) {
flags = isPSH ? (flags | 0x08) : (flags & 0xF7)
}
/// Sets or clears the ACK flag.
func setIsACK(_ isACK: Bool) {
flags = isACK ? (flags | 0x10) : (flags & 0xEF)
}
/// Returns the length of the TCP header.
func getTCPHeaderLength() -> Int {
return Int(dataOffset) * 4
}
/// Converts the TCP header to a byte array.
func toBytes() -> Data {
var buffer = Data()
buffer.append(contentsOf: sourcePort.bytes)
buffer.append(contentsOf: destinationPort.bytes)
buffer.append(contentsOf: sequenceNumber.bytes)
buffer.append(contentsOf: ackNumber.bytes)
//is ns and data offset
let headerLength = 5
buffer.append(UInt8((headerLength << 4) | (isNS ? 1 : 0)))
buffer.append(flags)
buffer.append(contentsOf: windowSize.bytes)
buffer.append(contentsOf: checksum.bytes)
buffer.append(contentsOf: urgentPointer.bytes)
// if let options = options {
// buffer.append(options)
// }
return buffer
}
/// Creates a copy of the TCP header.
func copy() -> TCPHeader {
return TCPHeader(
sourcePort: sourcePort,
destinationPort: destinationPort,
sequenceNumber: sequenceNumber,
ackNumber: ackNumber,
dataOffset: dataOffset,
isNS: isNS,
flags: flags,
windowSize: windowSize,
checksum: checksum,
urgentPointer: urgentPointer,
options: options
)
}
}
================================================
FILE: ios/ProxyPin/vpn/transport/protocol/TCPPacketFactory.swift
================================================
//
// TCPPacketFactory.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/16.
//
//
import Foundation
import os.log
/// Factory class for creating TCP packets.
class TCPPacketFactory {
public static let TCP_HEADER_LENGTH = 20
//从tcp报文创建tcpHeader
static func createTCPHeader(data: Data) -> TCPHeader? {
if data.count < TCP_HEADER_LENGTH {
os_log("Data is too short to be a TCP packet", log: OSLog.default, type: .error)
return nil
}
var offset = 0
func readUInt16() -> UInt16 {
let value = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt16.self).bigEndian }
offset += 2
return value
}
func readUInt32() -> UInt32 {
let value = data.withUnsafeBytes { $0.load(fromByteOffset: offset, as: UInt32.self).bigEndian }
offset += 4
return value
}
let sourcePort = readUInt16()
let destinationPort = readUInt16()
let sequenceNumber = readUInt32()
let ackNumber = readUInt32()
let dataOffsetAndReserved = data[offset]
offset += 1
let dataOffset = UInt8((dataOffsetAndReserved & 0xF0) >> 4)
let isNs = (dataOffsetAndReserved & 0x01) == 1
let flags = UInt8(data[offset])
offset += 1
let windowSize = readUInt16()
let checksum = readUInt16()
let urgentPointer = readUInt16()
var optionsSize = Int(dataOffset) - 5
var options: Data?
if (optionsSize > 0) {
optionsSize *= 4
options = data.subdata(in: offset.. Data {
var ip = ipHeader.copy()
var tcp = tcpHeader.copy()
flipIp(ip: &ip, tcp: &tcp)
let seqNumber = tcp.ackNumber
tcp.ackNumber = ackToClient
tcp.sequenceNumber = seqNumber
ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId())
// Set TCP flags
tcp.setIsACK(true)
tcp.setIsSYN(false)
tcp.setIsPSH(false)
tcp.setIsFIN(false)
tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes
tcp.options = nil
ip.totalLength = UInt16(ip.getIPHeaderLength() + tcp.getTCPHeaderLength())
return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil)
}
///创建带有RST标志的数据包,以便在需要重置时发送到客户端。
static func createRstData(ipHeader: IP4Header, tcpHeader: TCPHeader, dataLength: Int) -> Data {
var ip = ipHeader.copy()
var tcp = tcpHeader.copy()
var ackNumber: UInt32 = 0
var seqNumber: UInt32 = 0
if tcp.ackNumber > 0 {
seqNumber = tcp.ackNumber
} else {
ackNumber = tcp.sequenceNumber + UInt32(dataLength)
}
tcp.ackNumber = ackNumber
tcp.sequenceNumber = seqNumber
// Flip IP from source to destination
flipIp(ip: &ip, tcp: &tcp)
ip.identification = 0
tcp.flags = 0
tcp.isNS = false
tcp.setIsRST(true)
tcp.dataOffset = 5
tcp.options = nil
tcp.windowSize = 0
// Recalculate IP length
let totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()
ip.totalLength = UInt16(totalLength)
return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil)
}
//创建发送到客户端的FIN-ACK
static func createFinAckData(ipHeader: IP4Header, tcpHeader: TCPHeader, ackToClient: UInt32, seqToClient: UInt32, isFin: Bool, isAck: Bool) -> Data {
var ip = ipHeader.copy()
var tcp = tcpHeader.copy()
flipIp(ip: &ip, tcp: &tcp)
tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes
tcp.options = nil
tcp.ackNumber = ackToClient
tcp.sequenceNumber = seqToClient
ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId())
tcp.setIsACK(isAck)
tcp.setIsSYN(false)
tcp.setIsPSH(false)
tcp.setIsFIN(isFin)
ip.totalLength = UInt16(ip.getIPHeaderLength() + tcp.getTCPHeaderLength())
return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil)
}
//通过写回客户端流创建SYN-ACK数据包数据
public static func createSynAckPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader) -> Packet {
var ip = ipHeader.copy()
var tcp = tcpHeader.copy()
flipIp(ip: &ip, tcp: &tcp)
tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes
tcp.options = nil
// ack = received sequence + 1
let ackNumber = tcpHeader.sequenceNumber + 1
tcp.ackNumber = ackNumber
// Server-generated initial sequence number
let seqNumber = UInt64.random(in: 0..<100000)
tcp.sequenceNumber = UInt32(seqNumber)
// SYN-ACK
tcp.setIsACK(true)
tcp.setIsSYN(true)
tcp.timeStampReplyTo = tcp.timeStampSender
tcp.timeStampSender = PacketUtil.currentTime
ip.totalLength = UInt16(ip.getIPHeaderLength() + tcp.getTCPHeaderLength())
return Packet(ipHeader: ip, transportHeader: tcp, buffer: createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil))
}
//创建数据包数据以发送回客户端
public static func createResponsePacketData(
ipHeader: IP4Header, tcpHeader: TCPHeader, packetData: Data?, isPsh: Bool,
ackNumber: UInt32, seqNumber: UInt32, timeSender: Int, timeReplyTo: Int
) -> Data {
var ip = ipHeader.copy()
var tcp = tcpHeader.copy()
flipIp(ip: &ip, tcp: &tcp)
tcp.dataOffset = 5 // tcp header length 5 * 4 = 20 bytes
tcp.options = nil
tcp.ackNumber = ackNumber
tcp.sequenceNumber = seqNumber
ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId())
// ACK is always sent
tcp.setIsACK(true)
tcp.setIsSYN(false)
tcp.setIsPSH(isPsh)
tcp.setIsFIN(false)
tcp.timeStampSender = timeSender
tcp.timeStampReplyTo = timeReplyTo
var totalLength = ip.getIPHeaderLength() + tcp.getTCPHeaderLength()
if let packetData = packetData {
totalLength += packetData.count
}
ip.totalLength = UInt16(totalLength)
return createPacketData(ipHeader: ip, tcpHeader: tcp, data: packetData)
}
//将IP从源翻转到目标
private static func flipIp(ip: inout IP4Header, tcp: inout TCPHeader) {
let sourceIp = ip.destinationIP
let destIp = ip.sourceIP
let sourcePort = tcp.destinationPort
let destPort = tcp.sourcePort
ip.destinationIP = destIp
ip.sourceIP = sourceIp
tcp.destinationPort = destPort
tcp.sourcePort = sourcePort
}
public static func createFinData(
ipHeader: IP4Header, tcpHeader: TCPHeader, ackNumber: UInt32, seqNumber: UInt32,
timeSender: Int, timeReplyTo: Int
) -> Data {
var ip = ipHeader.copy()
var tcp = tcpHeader.copy()
flipIp(ip: &ip, tcp: &tcp)
tcp.ackNumber = ackNumber
tcp.sequenceNumber = seqNumber
ip.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId())
tcp.timeStampReplyTo = timeReplyTo
tcp.timeStampSender = timeSender
tcp.flags = 0
tcp.isNS = false
tcp.setIsACK(true)
tcp.setIsFIN(true)
tcp.options = nil
tcp.windowSize = 0
ip.totalLength = UInt16(ip.getIPHeaderLength() + TCP_HEADER_LENGTH)
return createPacketData(ipHeader: ip, tcpHeader: tcp, data: nil)
}
//从tcpHeader创建tcp报文
private static func createPacketData(ipHeader: IP4Header, tcpHeader: TCPHeader, data: Data?) -> Data {
let dataLength = data?.count ?? 0
var buffer = Data()
// Add IP header
let ipBuffer = ipHeader.toBytes()
buffer.append(ipBuffer)
// Add TCP header
let tcpBuffer = tcpHeader.toBytes()
buffer.append(tcpBuffer)
// Add data if exists
if let data = data {
buffer.append(data)
}
// Zero out IP checksum
buffer[10] = 0
buffer[11] = 0
// Calculate IP checksum
let ipChecksum = PacketUtil.calculateChecksum(data: buffer, offset: 0, length: ipBuffer.count)
buffer[10] = ipChecksum[0]
buffer[11] = ipChecksum[1]
// IPPacketFactory.printPacket(data: ipBuffer)
// Zero out TCP checksum
let tcpStart = ipBuffer.count
buffer[tcpStart + 16] = 0
buffer[tcpStart + 17] = 0
// Calculate TCP checksum
let tcpChecksum = PacketUtil.calculateTCPHeaderChecksum(
data: buffer, offset: tcpStart, tcpLength: tcpBuffer.count + dataLength,
sourceIP: ipHeader.sourceIP, destinationIP: ipHeader.destinationIP
)
buffer[tcpStart + 16] = tcpChecksum[0]
buffer[tcpStart + 17] = tcpChecksum[1]
return buffer
}
static func printPacket(data: Data) {
guard let tcpHeader = createTCPHeader(data: data) else {
os_log("Failed to create TCP header", log: OSLog.default, type: .error)
return
}
os_log("TCP Header: sourcePort: %{public}d, destinationPort: %{public}d, sequenceNumber: %{public}u, ackNumber: %{public}u, dataOffset: %{public}d, isNS: %{public}d, flags: %{public}d, windowSize: %{public}d, checksum: %{public}u, urgentPointer: %{public}u",
log: OSLog.default, type: .default, tcpHeader.sourcePort, tcpHeader.destinationPort, tcpHeader.sequenceNumber, tcpHeader.ackNumber, tcpHeader.dataOffset, tcpHeader.isNS ? 1 : 0, tcpHeader.flags, tcpHeader.windowSize, tcpHeader.checksum, tcpHeader.urgentPointer)
}
}
================================================
FILE: ios/ProxyPin/vpn/transport/protocol/TransportHeader.swift
================================================
//
// TransportHeader.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/17.
//
import Foundation
protocol TransportHeader {
func getSourcePort() -> Int
func getDestinationPort() -> Int
}
================================================
FILE: ios/ProxyPin/vpn/transport/protocol/UDPHeader.swift
================================================
//
// UDPHeader.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/17.
//
import Foundation
import os.log
///UDP报头的数据
struct UDPHeader {
var sourcePort: UInt16 //源端口号 16bit
var destinationPort: UInt16 //源端口号 16bit
var length: UInt16 //UDP数据报长度 16bit
var checksum: UInt16 //校验和 16bit
init(sourcePort: UInt16, destinationPort: UInt16, length: UInt16, checksum: UInt16) {
self.sourcePort = sourcePort
self.destinationPort = destinationPort
self.length = length
self.checksum = checksum
}
}
class UDPPacketFactory {
static let UDP_HEADER_LENGTH = 8
static func createUDPHeader(from data: Data) -> UDPHeader? {
guard data.count >= UDP_HEADER_LENGTH else {
return nil
}
let srcPort = data.withUnsafeBytes { $0.load(fromByteOffset: 0, as: UInt16.self).bigEndian }
let destPort = data.withUnsafeBytes { $0.load(fromByteOffset: 2, as: UInt16.self).bigEndian }
let length = data.withUnsafeBytes { $0.load(fromByteOffset: 4, as: UInt16.self).bigEndian }
let checksum = data.withUnsafeBytes { $0.load(fromByteOffset: 6, as: UInt16.self).bigEndian }
return UDPHeader(sourcePort: srcPort, destinationPort: destPort, length: length, checksum: checksum)
}
//
static func createResponsePacket(ip: IP4Header, udp: UDPHeader, packetData: Data?) -> Data {
var udpLen = 8
if let packetData = packetData {
udpLen += packetData.count
}
let srcPort = udp.destinationPort
let destPort = udp.sourcePort
let ipHeader = ip.copy()
let srcIp = ip.destinationIP
let destIp = ip.sourceIP
ipHeader.setMayFragment(false)
ipHeader.sourceIP = srcIp
ipHeader.destinationIP = destIp
ipHeader.identification = UInt16(truncatingIfNeeded: PacketUtil.getPacketId())
//ip的长度是整个数据包的长度 => IP header length + UDP header length (8) + UDP body length
let totalLength = ipHeader.getIPHeaderLength() + udpLen
ipHeader.totalLength = UInt16(totalLength)
var ipData = ipHeader.toBytes()
// clear IP checksum
ipData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) in
bytes[10] = 0
bytes[11] = 0
}
// calculate checksum for IP header
let ipChecksum = PacketUtil.calculateChecksum(data: ipData, offset: 0, length: ipData.count)
// write result of checksum back to buffer
ipData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) in
bytes[10] = ipChecksum[0]
bytes[11] = ipChecksum[1]
}
var buffer = Data()
// copy IP header to buffer
buffer.append(ipData)
// copy UDP header to buffer
buffer.append(contentsOf: srcPort.bytes)
buffer.append(contentsOf: destPort.bytes)
buffer.append(contentsOf: UInt16(udpLen).bytes)
// 计算UDP校验和
let udpChecksum: UInt16 = 0
buffer.append(contentsOf: udpChecksum.bytes)
if let packetData = packetData {
buffer.append(packetData)
}
return buffer
}
}
================================================
FILE: ios/ProxyPin/vpn/utils/PacketUtil.swift
================================================
//
// PacketUtil.swift
// ProxyPin
//
// Created by wanghongen on 2024/9/17.
//
import Foundation
import os.log
class PacketUtil {
private static var packetId: Int = 0
static func getPacketId() -> Int {
defer { packetId += 1 }
return packetId
}
static var currentTime: Int {
return Int(Date().timeIntervalSince1970)
}
static func writeIntToBytes(value: UInt32, buffer: inout Data, offset: Int) {
guard buffer.count >= offset + 4 else { return }
var intValue = value.bigEndian
let intData = Data(bytes: &intValue, count: 4)
buffer.replaceSubrange(offset.. String {
return String(format: "%d.%d.%d.%d", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF)
}
static func calculateTCPHeaderChecksum(data: Data, offset: Int, tcpLength: Int, sourceIP: UInt32, destinationIP: UInt32) -> Data {
var bufferSize = tcpLength + 12
var isOdd = false
if bufferSize % 2 != 0 {
bufferSize += 1
isOdd = true
}
var buffer = Data()
// Add source IP
buffer.append(contentsOf: sourceIP.bytes)
// Add destination IP
buffer.append(contentsOf: destinationIP.bytes)
// Add reserved byte and protocol (6 for TCP)
buffer.append(0)
buffer.append(6)
// Add TCP length
buffer.append(contentsOf: UInt16(tcpLength).bytes)
// Add TCP header and data
buffer.append(contentsOf: data[offset.. Data {
var start = offset
var sum = 0
while start < length {
sum += getNetworkInt(buffer: data, start: start, length: 2)
start += 2
}
// Carry over one's complement
while (sum >> 16) > 0 {
sum = (sum & 0xFFFF) + (sum >> 16)
}
// Flip the bits to get one's complement
sum = ~sum
// Extract the last two bytes of the int
let checksum = Data([UInt8(truncatingIfNeeded: (sum >> 8) & 0xFF), UInt8(truncatingIfNeeded: sum & 0xFF)])
return checksum
}
static func getNetworkInt(buffer: Data, start: Int, length: Int) -> Int {
var value = 0
var end = start + min(length, 4)
if end > buffer.count { end = buffer.count }
for i in start.. Bool {
guard let options = tcpHeader.options else {
return false
}
var i = 0
while i < options.count {
let kind = options[i]
switch kind {
case 0, 1:
break
case 2:
i += 3
case 3, 14:
i += 2
case 4:
i += 1
case 5, 15:
i += Int(options[i + 1]) - 2
case 8:
i += 9
case 23:
return true
default:
print("Unknown option: \(kind)")
}
i += 1
}
return false
}
}
extension FixedWidthInteger {
var bytes: [UInt8] {
withUnsafeBytes(of: self.bigEndian) { Array($0) }
}
}
================================================
FILE: ios/ProxyPin/vpn/utils/TLS.swift
================================================
//
// TLS.swift
// Runner
//
// Created by wanghongen on 2025/5/31.
//
class TLS {
static func isTLSClientHello(packetData: Data) -> Bool {
// Ensure the packet has enough data for a TLS ClientHello message
guard packetData.count >= 43 else {
return false
}
// Check if the first byte is 0x16 (Handshake type: ClientHello)
if packetData[0] != 0x16 {
return false
}
// Check if the next two bytes represent a valid TLS version (e.g., 0x0301, 0x0302, 0x0303)
let version = packetData[1...2]
if version != Data([0x03, 0x01]) && version != Data([0x03, 0x02]) && version != Data([0x03, 0x03]) {
return false
}
// Check if the handshake message type is ClientHello (0x01)
if packetData[5] != 0x01 {
return false
}
// Check if the record layer protocol version matches the expected TLS version
let recordVersion = packetData[9...10]
if recordVersion != Data([0x03, 0x01]) && recordVersion != Data([0x03, 0x02]) && recordVersion != Data([0x03, 0x03]) {
return false
}
return true
}
}
================================================
FILE: ios/Runner/AppDelegate.swift
================================================
import UIKit
import Flutter
import NetworkExtension
@main
@objc class AppDelegate: FlutterAppDelegate {
var backgroundAudioEnable: Bool = true
override func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
let vpnChannel = FlutterMethodChannel.init(name: "com.proxy/proxyVpn", binaryMessenger: controller as! FlutterBinaryMessenger);
vpnChannel.setMethodCallHandler({(call: FlutterMethodCall, result: FlutterResult) -> Void in
if ("stopVpn" == call.method) {
VpnManager.shared.disconnect()
} else if ("isRunning" == call.method){
result(Bool(VpnManager.shared.isRunning()))
} else if ("restartVpn" == call.method){
let arguments = call.arguments as? Dictionary
// VpnManager.shared.disconnect()
VpnManager.shared.restartConnect(host: arguments?["proxyHost"] as? String ,port: arguments?["proxyPort"] as? Int, ipProxy: arguments?["ipProxy"] as? Bool, proxyPassDomains: arguments?["proxyPassDomains"] as? [String])
} else {
let arguments = call.arguments as? Dictionary
VpnManager.shared.connect(host: arguments?["proxyHost"] as? String ,port: arguments?["proxyPort"] as? Int, ipProxy: arguments?["ipProxy"] as? Bool, proxyPassDomains: arguments?["proxyPassDomains"] as? [String])
}
})
if #available(iOS 13.0.0, *) {
PictureInPictureManager.regirst(flutter: controller as! FlutterBinaryMessenger)
MethodHandler.register(with: self.registrar(forPlugin: MethodHandler.name)!)
}
if let window = self.window {
window.rootViewController = controller
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func applicationWillTerminate(_ application: UIApplication) {
VpnManager.shared.disconnect()
}
var timer: Timer?
var bgTask: UIBackgroundTaskIdentifier?
override func applicationDidEnterBackground(_ application: UIApplication) {
if (!VpnManager.shared.isRunning()) {
return
}
timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
RunLoop.current.add(timer!, forMode: RunLoop.Mode.common)
bgTask = application.beginBackgroundTask(expirationHandler: nil)
}
@objc func timerAction() {
print(UIApplication.shared.backgroundTimeRemaining)
let application = UIApplication.shared
if (bgTask != nil) {
application.endBackgroundTask(bgTask!);
bgTask = nil;
}
if (UIApplication.shared.backgroundTimeRemaining < 60 && VpnManager.shared.isRunning()) {
bgTask = application.beginBackgroundTask(expirationHandler: nil)
}
if (application.backgroundTimeRemaining <= 0 || application.applicationState == .active || AudioManager.shared.openBackgroundAudioAutoplay) {
timer?.invalidate();
timer = nil;
}
if (application.backgroundTimeRemaining <= 10) {
self.backgroundAudio()
}
}
override func applicationWillResignActive(_ application: UIApplication) {
self.backgroundAudio();
}
override func applicationDidBecomeActive(_ application: UIApplication) {
self.endBackgroundUpdateTask()
}
private func backgroundAudio() {
if (!VpnManager.shared.isRunning() || !self.backgroundAudioEnable) {
return
}
if (AudioManager.shared.openBackgroundAudioAutoplay) {
return;
}
AudioManager.shared.openBackgroundAudioAutoplay = true
self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {
self.endBackgroundUpdateTask()
})
}
var backgroundUpdateTask: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier(rawValue: 0)
func endBackgroundUpdateTask() {
if (!VpnManager.shared.isRunning() || !AudioManager.shared.openBackgroundAudioAutoplay) {
return
}
AudioManager.shared.openBackgroundAudioAutoplay = false
UIApplication.shared.endBackgroundTask(self.backgroundUpdateTask)
self.backgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
}
}
================================================
FILE: ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images": [
{
"filename": "AppIcon@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "60x60"
},
{
"filename": "AppIcon@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "60x60"
},
{
"filename": "AppIcon~ipad.png",
"idiom": "ipad",
"scale": "1x",
"size": "76x76"
},
{
"filename": "AppIcon-40@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "40x40"
},
{
"filename": "AppIcon-40@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "40x40"
},
{
"filename": "AppIcon-20@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "20x20"
},
{
"filename": "AppIcon-20@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "20x20"
},
{
"filename": "AppIcon-29.png",
"idiom": "iphone",
"scale": "1x",
"size": "29x29"
},
{
"filename": "AppIcon-29@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "29x29"
},
{
"filename": "AppIcon-29@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "29x29"
},
{
"filename": "AppIcon-60@2x~car.png",
"idiom": "car",
"scale": "2x",
"size": "60x60"
},
{
"filename": "AppIcon-60@3x~car.png",
"idiom": "car",
"scale": "3x",
"size": "60x60"
},
{
"filename": "AppIcon@2x~ipad.png",
"idiom": "ipad",
"scale": "2x",
"size": "76x76"
},
{
"filename": "AppIcon-83.5@2x~ipad.png",
"idiom": "ipad",
"scale": "2x",
"size": "83.5x83.5"
},
{
"filename": "AppIcon~ios-marketing.png",
"idiom": "ios-marketing",
"scale": "1x",
"size": "1024x1024"
}
]
}
================================================
FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
================================================
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
================================================
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
================================================
FILE: ios/Runner/AudioManager.swift
================================================
import Foundation
import AVFoundation
import UIKit
class AudioManager: NSObject {
static let shared = AudioManager()
fileprivate let audioSession = AVAudioSession.sharedInstance()
fileprivate var backgroundAudioPlayer: AVAudioPlayer?
fileprivate var backgroundTimeLength = 0
fileprivate var timer: Timer?
static let audioName = ""
// 是否开启后台自动播放无声音乐
var openBackgroundAudioAutoplay = false {
didSet {
if self.openBackgroundAudioAutoplay {
self.setupAudioSession()
self.setupBackgroundAudioPlayer()
} else {
if let player = self.backgroundAudioPlayer {
if player.isPlaying {
player.stop()
}
}
self.backgroundAudioPlayer = nil
try? self.audioSession.setActive(false, options: AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation)
}
}
}
override init() {
super.init()
self.setupListener()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
private func setupAudioSession() {
do {
try self.audioSession.setCategory(AVAudioSession.Category.playback, options: AVAudioSession.CategoryOptions.mixWithOthers)
try self.audioSession.setActive(false)
} catch let error {
debugPrint("\(type(of:self)):\(error)")
}
}
private func setupBackgroundAudioPlayer() {
do {
self.backgroundAudioPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: Bundle.main.path(forResource: "silence", ofType: "mp3")!))
} catch let error {
debugPrint("\(type(of:self)):\(error)")
}
self.backgroundAudioPlayer?.numberOfLoops = -1
self.backgroundAudioPlayer?.volume = 0
self.backgroundAudioPlayer?.delegate = self
}
private func setupListener() {
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(audioSessionInterruption(notification:)), name: AVAudioSession.interruptionNotification, object: nil)
}
}
// MARK: - 扩展 监听通知
extension AudioManager {
/// 进入后台 播放无声音乐
@objc public func didEnterBackground() {
self.setupTimer()
guard self.openBackgroundAudioAutoplay else {return}
do {
try self.audioSession.setActive(true)
} catch let error {
debugPrint("\(type(of:self)):\(error))")
}
self.backgroundAudioPlayer?.prepareToPlay()
self.backgroundAudioPlayer?.play()
}
/// 进入前台,暂停播放音乐
@objc public func didBecomeActive() {
self.removeTimer()
self.hintBackgroundTimeLength()
self.backgroundTimeLength = 0
guard self.openBackgroundAudioAutoplay else {return}
self.backgroundAudioPlayer?.pause()
do {
try self.audioSession.setActive(false, options: AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation)
} catch let error {
debugPrint("\(type(of:self)):\(error))")
}
}
/// 音乐中断处理
@objc fileprivate func audioSessionInterruption(notification: NSNotification) {
guard self.openBackgroundAudioAutoplay else {return}
guard let userinfo = notification.userInfo else {return}
guard let interruptionType: UInt = userinfo[AVAudioSessionInterruptionTypeKey] as! UInt? else {return}
if interruptionType == AVAudioSession.InterruptionType.began.rawValue {
// 中断开始,音乐被暂停
debugPrint("\(type(of:self)): 中断开始 userinfo:\(userinfo)")
} else if interruptionType == AVAudioSession.InterruptionType.ended.rawValue {
// 中断结束,恢复播放
debugPrint("\(type(of:self)): 中断结束 userinfo:\(userinfo)")
guard let player = self.backgroundAudioPlayer else {return}
if player.isPlaying == false {
debugPrint("\(type(of:self)): 音乐未播放,准备开始播放")
do {
try self.audioSession.setActive(true)
} catch let error {
debugPrint("\(type(of:self)):\(error)")
}
player.prepareToPlay()
player.play()
} else {
debugPrint("\(type(of:self)): 音乐正在播放")
}
}
}
}
// MARK: - 扩展 定时器任务
extension AudioManager {
fileprivate func setupTimer() {
self.removeTimer()
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(timerTask), userInfo: nil, repeats: true)
RunLoop.main.add(self.timer!, forMode: RunLoop.Mode.common)
// RunLoop.main.add(self.timer!, forMode: RunLoop.Mode.init(rawValue: ""))
}
fileprivate func removeTimer() {
self.timer?.invalidate()
self.timer = nil;
}
@objc func timerTask() {
self.backgroundTimeLength += 1
}
fileprivate func hintBackgroundTimeLength() {
let message = "本次后台持续时间:\(self.backgroundTimeLength)s"
print(message)
}
}
// MARK: - 扩展 播放代理
extension AudioManager: AVAudioPlayerDelegate {
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
}
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
debugPrint("\(type(of:self))" + error.debugDescription)
}
}
================================================
FILE: ios/Runner/Base.lproj/LaunchScreen.storyboard
================================================
================================================
FILE: ios/Runner/Base.lproj/Main.storyboard
================================================
================================================
FILE: ios/Runner/Handlers/MethodHandler.swift
================================================
//
// MethodHandler.swift
// Runner
//
// Created by wanghongen on 2025/5/30.
//
import Flutter
import Network
import SystemConfiguration.CaptiveNetwork
import Security
public class MethodHandler: NSObject, FlutterPlugin {
public static let name = "com.proxypin/method"
private var channel: FlutterMethodChannel?
private var currentPathMonitor: NWPathMonitor?
private var currentCompletionHandler: ((Bool) -> Void)?
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: Self.name, binaryMessenger: registrar.messenger())
let instance = MethodHandler()
registrar.addMethodCallDelegate(instance, channel: channel)
instance.channel = channel
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "requestLocalNetwork":
// 调用异步函数,并在其完成时传递结果
self.requestLocalNetworkAccess { isAvailable in
print("[MethodHandler] requestLocalNetwork result: \(isAvailable)")
result(isAvailable)
}
case "isCaInstalled":
guard let args = call.arguments as? [String: Any], let pem = args["pem"] as? String else {
print("[MethodHandler] isCaInstalled ARG_ERROR: missing pem")
result(FlutterError(code: "ARG_ERROR", message: "Missing pem", details: nil))
return
}
let ret = self.isCertificateInstalled(pem: pem)
result(ret)
case "evaluateChainTrusted":
guard let args = call.arguments as? [String: Any], let leafPem = args["leafPem"] as? String, let caPem = args["caPem"] as? String else {
print("[MethodHandler] evaluateChainTrusted ARG_ERROR: missing leafPem/caPem")
result(FlutterError(code: "ARG_ERROR", message: "Missing leafPem/caPem", details: nil))
return
}
let host = args["host"] as? String
let ret = self.isChainTrusted(leafPem: leafPem, caPem: caPem, host: host)
// print("[MethodHandler] evaluateChainTrusted => \(ret)")
result(ret)
default:
print("[MethodHandler] method not implemented: \(call.method)")
result(FlutterMethodNotImplemented)
}
}
// MARK: - iOS: Check certificate trust
private func isCertificateInstalled(pem: String) -> Bool {
guard let der = self.decodePemToDer(pem) as CFData?, let certificate = SecCertificateCreateWithData(nil, der) else {
print("[MethodHandler] isCertificateTrusted decode/create cert failed")
return false
}
let policy = SecPolicyCreateBasicX509()
var trust: SecTrust?
let status = SecTrustCreateWithCertificates(certificate, policy, &trust)
if status != errSecSuccess || trust == nil {
print("[MethodHandler] SecTrustCreateWithCertificates failed status=\(status)")
return false
}
if #available(iOS 12.0, *) {
var error: CFError?
let ok = SecTrustEvaluateWithError(trust!, &error)
if let e = error {
print("[MethodHandler] SecTrustEvaluateWithError ok=\(ok) error=\(e)")
}
return ok
} else {
var trustResult = SecTrustResultType.invalid
let evalStatus = SecTrustEvaluate(trust!, &trustResult)
let ok = (evalStatus == errSecSuccess) && (trustResult == .unspecified || trustResult == .proceed)
print("[MethodHandler] SecTrustEvaluate status=\(evalStatus) result=\(trustResult.rawValue) trusted=\(ok)")
return ok
}
}
// MARK: - iOS: Evaluate leaf+CA chain with SSL policy
private func isChainTrusted(leafPem: String, caPem: String, host: String?) -> Bool {
guard let leafDer = self.decodePemToDer(leafPem) as CFData?, let leaf = SecCertificateCreateWithData(nil, leafDer) else {
print("[MethodHandler] isChainTrusted leaf decode/create failed")
return false
}
guard let caDer = self.decodePemToDer(caPem) as CFData?, let ca = SecCertificateCreateWithData(nil, caDer) else {
print("[MethodHandler] isChainTrusted ca decode/create failed")
return false
}
let certs: [SecCertificate] = [leaf, ca]
let policy = SecPolicyCreateSSL(true, host as CFString?)
var trust: SecTrust?
let status = SecTrustCreateWithCertificates(certs as CFTypeRef, policy, &trust)
if status != errSecSuccess || trust == nil {
print("[MethodHandler] isChainTrusted SecTrustCreateWithCertificates failed status=\(status)")
return false
}
if #available(iOS 12.0, *) {
var error: CFError?
let ok = SecTrustEvaluateWithError(trust!, &error)
if let e = error { print("[MethodHandler] isChainTrusted evaluate ok=\(ok) error=\(e)") } else { print("[MethodHandler] isChainTrusted evaluate ok=\(ok)") }
return ok
} else {
var trustResult = SecTrustResultType.invalid
let evalStatus = SecTrustEvaluate(trust!, &trustResult)
let ok = (evalStatus == errSecSuccess) && (trustResult == .unspecified || trustResult == .proceed)
// print("[MethodHandler] isChainTrusted evaluate status=\(evalStatus) result=\(trustResult.rawValue) trusted=\(ok)")
return ok
}
}
private func decodePemToDer(_ pem: String) -> Data? {
// Strip header/footer and whitespace
let lines = pem.components(separatedBy: "\n").filter { line in
return !line.contains("-----BEGIN") && !line.contains("-----END") && !line.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
let base64Str = lines.joined()
let der = Data(base64Encoded: base64Str, options: .ignoreUnknownCharacters)
return der
}
/// 异步检查本地网络(Wi-Fi 或以太网)是否可用。
/// - Parameter completion: 一个回调函数,当检查完成时调用,参数为 Bool 类型,true 表示本地网络可用,false 表示不可用。
func requestLocalNetworkAccess(completion: @escaping (Bool) -> Void) {
// 如果已有正在进行的监视,先取消它
self.currentPathMonitor?.cancel()
self.currentPathMonitor = NWPathMonitor()
// 将 completion 存储起来,以便在 pathUpdateHandler 中调用
// 这是为了确保 completion 只被调用一次
self.currentCompletionHandler = completion
self.currentPathMonitor?.pathUpdateHandler = { [weak self] path in
guard let self = self else { return }
// 确保 completionHandler 仍然存在(即尚未被调用和清除)
guard let completionHandler = self.currentCompletionHandler else {
// 可能已经被调用过了,或者监视器被意外触发
// 为安全起见,取消监视器
self.currentPathMonitor?.cancel()
return
}
var isLocalNetworkAvailable = false
print("Network path status: \(path.status)")
if path.status == .satisfied {
if path.usesInterfaceType(.wifi) || path.usesInterfaceType(.wiredEthernet) {
isLocalNetworkAvailable = true
}
}
// 对于其他状态 (例如 .unsatisfied, .requiresConnection) 或其他接口类型 (例如 cellular),
// isLocalNetworkAvailable 将保持 false。
// 调用存储的 completion handler
completionHandler(isLocalNetworkAvailable)
// 清理:取消监视器并清除存储的引用,以防止重复调用和内存泄漏
self.currentPathMonitor?.cancel()
self.currentPathMonitor = nil
self.currentCompletionHandler = nil
}
// 在主队列上启动监视器
self.currentPathMonitor?.start(queue: DispatchQueue.global())
}
}
================================================
FILE: ios/Runner/Info.plist
================================================
BGTaskSchedulerPermittedIdentifiers
$(PRODUCT_BUNDLE_IDENTIFIER)
CADisableMinimumFrameDurationOnPhone
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
ProxyPin
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleLocalizations
en
zh
CFBundleName
ProxyPin
CFBundlePackageType
APPL
CFBundleShortVersionString
$(FLUTTER_BUILD_NAME)
CFBundleSignature
????
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
NSAppTransportSecurity
NSAllowsArbitraryLoads
NSCameraUsageDescription
Scan QR code
NSPhotoLibraryUsageDescription
Access to Photo Library
UIApplicationSupportsIndirectInputEvents
UIBackgroundModes
audio
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
Main
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
UISupportedInterfaceOrientations~ipad
UIInterfaceOrientationPortrait
UIInterfaceOrientationPortraitUpsideDown
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
UIViewControllerBasedStatusBarAppearance
NSAppTransportSecurity
NSAllowsArbitraryLoads
NSLocalNetworkUsageDescription
Remote Device Connect
================================================
FILE: ios/Runner/Runner-Bridging-Header.h
================================================
#import "GeneratedPluginRegistrant.h"
================================================
FILE: ios/Runner/Runner.entitlements
================================================
aps-environment
development
com.apple.developer.networking.networkextension
packet-tunnel-provider
com.apple.developer.networking.vpn.api
allow-vpn
com.apple.security.application-groups
group.com.proxy.pin
com.apple.security.network.client
com.apple.security.network.server
================================================
FILE: ios/Runner/VpnManager.swift
================================================
let kProxyServiceVPNStatusNotification = "kProxyServiceVPNStatusNotification"
import Foundation
import NetworkExtension
enum VPNStatus {
case off
case connecting
case on
case disconnecting
}
class VpnManager{
var activeVPN: NETunnelProviderManager?;
public var proxyHost: String = "127.0.0.1"
public var proxyPort: Int = 9099
public var ipProxy: Bool = false
public var proxyPassDomains: [String]?
static let shared = VpnManager()
var observerAdded: Bool = false
fileprivate(set) var vpnStatus = VPNStatus.off {
didSet {
NotificationCenter.default.post(name: Notification.Name(rawValue: kProxyServiceVPNStatusNotification), object: nil)
}
}
init() {
loadProviderManager{
guard let manager = $0 else{return}
self.updateVPNStatus(manager)
}
addVPNStatusObserver()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func addVPNStatusObserver() {
guard !observerAdded else{
return
}
loadProviderManager { [unowned self] (manager) -> Void in
if let manager = manager {
self.observerAdded = true
NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: manager.connection, queue: OperationQueue.main, using: { [unowned self] (notification) -> Void in
self.updateVPNStatus(manager)
if (manager.connection.status == .invalid || manager.connection.status == .disconnected){
print("VPN断开: \(String(describing: manager.debugDescription))")
}
})
}
}
}
func updateVPNStatus(_ manager: NEVPNManager) {
switch manager.connection.status {
case .connected:
self.vpnStatus = .on
case .connecting, .reasserting:
self.vpnStatus = .connecting
case .disconnecting:
self.vpnStatus = .disconnecting
case .disconnected, .invalid:
self.vpnStatus = .off
@unknown default: break
}
}
}
// load VPN Profiles
extension VpnManager{
fileprivate func createProviderManager() -> NETunnelProviderManager {
let manager = NETunnelProviderManager()
let conf = NETunnelProviderProtocol()
conf.serverAddress = "ProxyPin"
manager.protocolConfiguration = conf
manager.localizedDescription = "ProxyPin"
return manager
}
func loadAndCreatePrividerManager(_ complete: @escaping (NETunnelProviderManager?) -> Void ){
NETunnelProviderManager.loadAllFromPreferences{ [self] (managers, error) in
guard let managers = managers else{return}
let manager: NETunnelProviderManager
if managers.count > 0 {
manager = managers[0]
}else{
manager = self.createProviderManager()
}
var conf = [String:AnyObject]()
conf["proxyHost"] = self.proxyHost as AnyObject
conf["proxyPort"] = self.proxyPort as AnyObject
conf["ipProxy"] = self.ipProxy as AnyObject
// Bridge Swift [String] to NSArray (Objective-C) before inserting into AnyObject dictionary
if let passDomains = self.proxyPassDomains {
conf["proxyPassDomains"] = passDomains as NSArray
}
let orignConf = manager.protocolConfiguration as! NETunnelProviderProtocol
orignConf.providerConfiguration = conf
manager.protocolConfiguration = orignConf
print(orignConf)
manager.isEnabled = true
manager.saveToPreferences{
if ($0 != nil){
// complete(nil);
// return;
}
manager.loadFromPreferences{
if $0 != nil{
print("loadFromPreferences",$0.debugDescription)
complete(nil);return;
}
self.addVPNStatusObserver()
complete(manager)
}
}
}
}
func loadProviderManager(_ complete: @escaping (NETunnelProviderManager?) -> Void){
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
if let managers = managers {
if managers.count > 0 {
let manager = managers[0]
complete(manager)
return
}
}
complete(nil)
}
}
}
// Actions
extension VpnManager{
func connect(host: String?, port: Int?, ipProxy: Bool? = false, proxyPassDomains: [String]? = nil) {
self.proxyHost = host ?? self.proxyHost
self.proxyPort = port ?? self.proxyPort
self.ipProxy = ipProxy ?? false
self.proxyPassDomains = proxyPassDomains ?? self.proxyPassDomains
self.loadAndCreatePrividerManager { (manager) in
guard let manager = manager else{return}
do{
self.activeVPN = manager
try manager.connection.startVPNTunnel()
}catch let err{
print("connect: ", err)
}
}
}
func restartConnect(host: String?, port: Int?, ipProxy: Bool? = false, proxyPassDomains: [String]? = nil) {
self.proxyHost = host ?? self.proxyHost
self.proxyPort = port ?? self.proxyPort
self.ipProxy = ipProxy ?? false
if (activeVPN != nil) {
activeVPN?.connection.stopVPNTunnel()
activeVPN = nil
}
self.connect(host: host, port: port, ipProxy: ipProxy, proxyPassDomains: proxyPassDomains)
}
func disconnect() {
if (activeVPN != nil) {
activeVPN?.connection.stopVPNTunnel()
activeVPN = nil
return
}
loadProviderManager{
$0?.connection.stopVPNTunnel()
}
}
func isRunning() -> Bool {
return vpnStatus == VPNStatus.on
}
}
================================================
FILE: ios/Runner/en.lproj/InfoPlist.strings
================================================
"NSCameraUsageDescription"="Scan QR code";
"NSPhotoLibraryUsageDescription"="Access to Photo Library";
"PhotoLibraryAddUsageDescription"= "Save image to Photo Library";
================================================
FILE: ios/Runner/pip/PictureInPictureManager.swift
================================================
//
// PictureInPicturePlugin.swift
// Runner
//
// Created by wanghongen on 2024/1/8.
//
import AVKit
import UIKit
import Flutter
import SnapKit
import SwiftUI
@available(iOS 13.0.0, *)
class PictureInPictureManager: NSObject,AVPictureInPictureControllerDelegate {
static var shared: PictureInPictureManager!
private var channel: FlutterMethodChannel;
//播放器
private var playerLayer: AVPlayerLayer?
// 画中画
var pipController: AVPictureInPictureController!
var pipView: PictureInPictureView?
var proxyPort :Int = -1;
static func regirst(flutter: FlutterBinaryMessenger) {
let channel = FlutterMethodChannel.init(name: "com.proxy/pictureInPicture", binaryMessenger: flutter);
shared = PictureInPictureManager(channel: channel)
}
private init(channel: FlutterMethodChannel) {
self.channel = channel
super.init()
channel.setMethodCallHandler({(call: FlutterMethodCall, result: FlutterResult) -> Void in
// print("画中画 {call.method} methodCallHandler:\(UIApplication.shared.windows)")
if ("enterPictureInPictureMode" == call.method) {
let arguments = call.arguments as? Dictionary
self.proxyPort = arguments?["proxyPort"] as! Int
self.starPiP()
result(Bool(true))
} else if ("addData" == call.method) {
self.pipView?.addData(text: call.arguments as! String)
}
})
if AVPictureInPictureController.isPictureInPictureSupported() {
do {
try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
} catch {
print(error)
}
}
}
private func initPIP() {
if (playerLayer == nil) {
setupPlayer()
}
if (pipController == nil) {
print("画中画初始化:\(UIApplication.shared.windows)")
setupPip()
}
}
// 配置播放器
private func setupPlayer() {
let video = Bundle.main.url(forResource: "silience", withExtension: "mov")
let asset = AVAsset.init(url: video!)
let playerItem = AVPlayerItem.init(asset: asset)
let player = AVPlayer.init(playerItem: playerItem)
playerLayer = AVPlayerLayer(player: player)
playerLayer?.frame = .init(x: 90, y: 390, width: 180, height: 280)
playerLayer?.isHidden = true
player.isMuted = true
player.allowsExternalPlayback = true
// player.play()
let view = UIView()
view.layer.addSublayer(playerLayer!)
UIApplication.shared.keyWindow?.rootViewController?.view.addSubview(view)
}
// 配置画中画
private func setupPip() {
pipController = AVPictureInPictureController.init(playerLayer: playerLayer!)!
pipController.delegate = self
// if #available(iOS 14.2, *) {
// pipController.canStartPictureInPictureAutomaticallyFromInline = true
// }
// 隐藏播放按钮、快进快退按钮
pipController.setValue(1, forKey: "controlsStyle")
//点击回到app
//pipController.setValue(2, forKey: "controlsStyle")
}
// 开启/关闭 画中画
func starPiP() {
self.initPIP();
if pipController.isPictureInPictureActive {
pipController.stopPictureInPicture()
} else {
print("starPiP \(pipController.isPictureInPicturePossible)")
if (pipController.isPictureInPicturePossible) {
pipController.startPictureInPicture()
return;
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) { [self] in
if (self.pipController.isPictureInPicturePossible) {
self.pipController.startPictureInPicture()
return;
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
self.pipController.startPictureInPicture()
}
}
}
}
var playButton = UIButton(type: .custom)
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// print("画中画初始化后:\(UIApplication.shared.windows)")
// 把自定义view加到画中画上
if let window = UIApplication.shared.windows.first {
pipView = PictureInPictureView()
let vc = UIHostingController(rootView: pipView)
let icon = VpnManager.shared.isRunning() ? "pause.fill" : "play.fill"
playButton.setImage(UIImage(systemName: icon), for: .normal)
playButton.addTarget(self, action: #selector(vpnAction), for: .touchUpInside)
vc.view.addSubview(playButton)
playButton.snp.makeConstraints{ (make) in
make.left.equalTo(15)
make.bottom.equalTo(-13)
}
let clearButton = UIButton(type: .custom)
clearButton.setImage(UIImage(systemName: "trash.circle"), for: .normal)
clearButton.addTarget(self, action: #selector(cleanAction), for: .touchUpInside)
vc.view.addSubview(clearButton)
clearButton.snp.makeConstraints{ (make) in
make.right.equalTo(-13)
make.bottom.equalTo(-13)
}
window.addSubview(vc.view!)
// 使用自动布局
vc.view?.snp.makeConstraints { (make) -> Void in
make.edges.equalToSuperview()
}
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
}
}
@objc func cleanAction() {
channel.invokeMethod("cleanSession", arguments: nil)
pipView?.dataSource.clear()
}
@objc func vpnAction() {
if (VpnManager.shared.isRunning()) {
VpnManager.shared.disconnect()
playButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
} else {
VpnManager.shared.connect(host: nil, port: proxyPort, ipProxy: nil)
playButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
}
// pipView?.addData(text: "hello")
}
func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// print("pictureInPictureControllerWillStopPictureInPicture:")
channel.invokeMethod("exitPictureInPictureMode", arguments: nil)
}
}
================================================
FILE: ios/Runner/pip/PictureInPictureView.swift
================================================
//
// PictureInPictureView.swift
// Runner
//
// Created by wanghongen on 2024/1/9.
//
import SwiftUI
@available(iOS 13.0, *)
class DataSource: ObservableObject {
@Published var list: [String] = []
func clear() {
list.removeAll()
}
}
@available(iOS 13.0, *)
struct PictureInPictureView: View {
@ObservedObject var dataSource = DataSource()
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 1.3){
ForEach((0.. Void in
// make.edges.equalToSuperview()
// }
// }
//
// func addData(text: String) {
// let str = "• " + text + "\n" + (viewLabel.text ?? "");
// self.viewLabel.text = str;
// }
//}
================================================
FILE: ios/Runner/zh-Hans.lproj/InfoPlist.strings
================================================
"NSCameraUsageDescription"="扫描二维码";
"NSPhotoLibraryUsageDescription"="访问相册";
"PhotoLibraryAddUsageDescription"= "保存图片到相册";
================================================
FILE: ios/Runner/zh-Hans.lproj/LaunchScreen.strings
================================================
================================================
FILE: ios/Runner/zh-Hans.lproj/Main.strings
================================================
================================================
FILE: ios/Runner.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
1FBB39B834EBBDA7C793EA99 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 239046BD4495108B4DFCCCB4 /* Pods_Runner.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
9B09121B2A5457B3001108B7 /* VpnManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B09121A2A5457B3001108B7 /* VpnManager.swift */; };
9B0912222A54593A001108B7 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B0912212A54593A001108B7 /* NetworkExtension.framework */; };
9B0912252A54593A001108B7 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B0912242A54593A001108B7 /* PacketTunnelProvider.swift */; };
9B09122A2A54593A001108B7 /* ProxyPin.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 9B0912202A54593A001108B7 /* ProxyPin.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
9B2A10C62B4CA9A6001C443F /* PictureInPictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2A10C52B4CA9A6001C443F /* PictureInPictureView.swift */; };
9B2A10C82B4CBE32001C443F /* silience.mov in Resources */ = {isa = PBXBuildFile; fileRef = 9B2A10C72B4CBE32001C443F /* silience.mov */; };
9B5125AA2CAEE3350027996E /* ICMPPacket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B5125A92CAEE3350027996E /* ICMPPacket.swift */; };
9B70772D2A5718FB00F184A9 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B70772C2A5718FB00F184A9 /* AudioManager.swift */; };
9B7077362A5728B900F184A9 /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 9B7077352A5728B900F184A9 /* silence.mp3 */; };
9B90F5802C183CDE007D7A81 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9B90F5822C183CDE007D7A81 /* InfoPlist.strings */; };
9BAB4FC02DE75CFE0093BFBA /* GBPing.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BAB4FBC2DE75CFE0093BFBA /* GBPing.m */; };
9BAB4FC12DE75CFE0093BFBA /* GBPingSummary.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BAB4FBE2DE75CFE0093BFBA /* GBPingSummary.m */; };
9BAB4FC32DE75D220093BFBA /* GBPingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAB4FC22DE75D220093BFBA /* GBPingHelper.swift */; };
9BC4B8CC2B4B48710047DBDD /* PictureInPictureManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC4B8CB2B4B48710047DBDD /* PictureInPictureManager.swift */; };
9BCA28662C9772DD00C2B46C /* ConnectionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28652C9772DD00C2B46C /* ConnectionHandler.swift */; };
9BCA286A2C97748100C2B46C /* IP4Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28692C97748100C2B46C /* IP4Header.swift */; };
9BCA286D2C977E3800C2B46C /* TCPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA286C2C977E3800C2B46C /* TCPHeader.swift */; };
9BCA286F2C977E4C00C2B46C /* TCPPacketFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA286E2C977E4C00C2B46C /* TCPPacketFactory.swift */; };
9BCA28712C987B0C00C2B46C /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28702C987B0B00C2B46C /* ConnectionManager.swift */; };
9BCA28732C988E9D00C2B46C /* Packet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28722C988E9D00C2B46C /* Packet.swift */; };
9BCA28752C988EC400C2B46C /* TransportHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28742C988EC400C2B46C /* TransportHeader.swift */; };
9BCA28782C98902900C2B46C /* PacketUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28772C98902900C2B46C /* PacketUtil.swift */; };
9BCA287A2C989A7200C2B46C /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28792C989A7200C2B46C /* Connection.swift */; };
9BCA287D2C989A9F00C2B46C /* CloseableConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA287C2C989A9F00C2B46C /* CloseableConnection.swift */; };
9BCA287F2C989AF300C2B46C /* NWProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA287E2C989AF300C2B46C /* NWProtocol.swift */; };
9BCA28812C98A42A00C2B46C /* ClientPacketWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28802C98A42A00C2B46C /* ClientPacketWriter.swift */; };
9BCA28832C98AA9000C2B46C /* ProxyVpnService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28822C98AA9000C2B46C /* ProxyVpnService.swift */; };
9BCA28852C98C6B300C2B46C /* QueueFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28842C98C6B300C2B46C /* QueueFactory.swift */; };
9BCA288A2C98C82000C2B46C /* SocketIOService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA28892C98C82000C2B46C /* SocketIOService.swift */; };
9BCA288C2C995B3700C2B46C /* UDPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCA288B2C995B3700C2B46C /* UDPHeader.swift */; };
9BE87B5C2DEA480000F4FCEF /* MethodHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE87B5B2DEA47FA00F4FCEF /* MethodHandler.swift */; };
9BE87B5E2DEA6BAE00F4FCEF /* TLS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE87B5D2DEA6BAB00F4FCEF /* TLS.swift */; };
B375908E625E0AED772FA2C0 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D37E307095F2B3E689A68827 /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
9B0912282A54593A001108B7 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 9B09121F2A54593A001108B7;
remoteInfo = ProxyPin;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
9B09122B2A54593B001108B7 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
9B09122A2A54593A001108B7 /* ProxyPin.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
0B67A4E592FF13260AAFD656 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
239046BD4495108B4DFCCCB4 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
306514357AC94BE3DDEBC8D8 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
3E54CF83D4EE560125987C8A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
72900351EF1A3F028032459A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
8215052AB7CBF47CD3DAAF69 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
9B0912192A545757001108B7 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
9B09121A2A5457B3001108B7 /* VpnManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VpnManager.swift; sourceTree = ""; };
9B0912202A54593A001108B7 /* ProxyPin.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ProxyPin.appex; sourceTree = BUILT_PRODUCTS_DIR; };
9B0912212A54593A001108B7 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
9B0912242A54593A001108B7 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; };
9B0912262A54593A001108B7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
9B0912272A54593A001108B7 /* ProxyPin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ProxyPin.entitlements; sourceTree = ""; };
9B2A10C52B4CA9A6001C443F /* PictureInPictureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureInPictureView.swift; sourceTree = ""; };
9B2A10C72B4CBE32001C443F /* silience.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = silience.mov; sourceTree = ""; };
9B5125A92CAEE3350027996E /* ICMPPacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICMPPacket.swift; sourceTree = ""; };
9B70772C2A5718FB00F184A9 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; };
9B7077352A5728B900F184A9 /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = ""; };
9B90F57C2C183C7E007D7A81 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; };
9B90F57D2C183C7E007D7A81 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = ""; };
9B90F5812C183CDE007D7A81 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; };
9B90F5832C183CE0007D7A81 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; };
9BAB4FBB2DE75CFE0093BFBA /* GBPing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GBPing.h; sourceTree = ""; };
9BAB4FBC2DE75CFE0093BFBA /* GBPing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GBPing.m; sourceTree = ""; };
9BAB4FBD2DE75CFE0093BFBA /* GBPingSummary.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GBPingSummary.h; sourceTree = ""; };
9BAB4FBE2DE75CFE0093BFBA /* GBPingSummary.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GBPingSummary.m; sourceTree = ""; };
9BAB4FBF2DE75CFE0093BFBA /* ICMPHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ICMPHeader.h; sourceTree = ""; };
9BAB4FC22DE75D220093BFBA /* GBPingHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GBPingHelper.swift; sourceTree = ""; };
9BAB4FC42DE75E9A0093BFBA /* ProxyPin-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ProxyPin-Bridging-Header.h"; sourceTree = ""; };
9BC4B8CB2B4B48710047DBDD /* PictureInPictureManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureInPictureManager.swift; sourceTree = ""; };
9BCA28652C9772DD00C2B46C /* ConnectionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionHandler.swift; sourceTree = ""; };
9BCA28692C97748100C2B46C /* IP4Header.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IP4Header.swift; sourceTree = ""; };
9BCA286C2C977E3800C2B46C /* TCPHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPHeader.swift; sourceTree = ""; };
9BCA286E2C977E4C00C2B46C /* TCPPacketFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCPPacketFactory.swift; sourceTree = ""; };
9BCA28702C987B0B00C2B46C /* ConnectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionManager.swift; sourceTree = ""; };
9BCA28722C988E9D00C2B46C /* Packet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Packet.swift; sourceTree = ""; };
9BCA28742C988EC400C2B46C /* TransportHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportHeader.swift; sourceTree = ""; };
9BCA28772C98902900C2B46C /* PacketUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketUtil.swift; sourceTree = ""; };
9BCA28792C989A7200C2B46C /* Connection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connection.swift; sourceTree = ""; };
9BCA287C2C989A9F00C2B46C /* CloseableConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseableConnection.swift; sourceTree = ""; };
9BCA287E2C989AF300C2B46C /* NWProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWProtocol.swift; sourceTree = ""; };
9BCA28802C98A42A00C2B46C /* ClientPacketWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientPacketWriter.swift; sourceTree = ""; };
9BCA28822C98AA9000C2B46C /* ProxyVpnService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyVpnService.swift; sourceTree = ""; };
9BCA28842C98C6B300C2B46C /* QueueFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueFactory.swift; sourceTree = ""; };
9BCA28892C98C82000C2B46C /* SocketIOService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketIOService.swift; sourceTree = ""; };
9BCA288B2C995B3700C2B46C /* UDPHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPHeader.swift; sourceTree = ""; };
9BE87B5B2DEA47FA00F4FCEF /* MethodHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodHandler.swift; sourceTree = ""; };
9BE87B5D2DEA6BAB00F4FCEF /* TLS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TLS.swift; sourceTree = ""; };
D37E307095F2B3E689A68827 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E328C7F89A365CDC0EAD15C6 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
2C2BB3BDC059E8FD67F7FF64 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B375908E625E0AED772FA2C0 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
1FBB39B834EBBDA7C793EA99 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
9B09121D2A54593A001108B7 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9B0912222A54593A001108B7 /* NetworkExtension.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
28892733E959FF4F4696A049 /* Frameworks */ = {
isa = PBXGroup;
children = (
239046BD4495108B4DFCCCB4 /* Pods_Runner.framework */,
D37E307095F2B3E689A68827 /* Pods_RunnerTests.framework */,
9B0912212A54593A001108B7 /* NetworkExtension.framework */,
);
name = Frameworks;
sourceTree = "";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "";
};
8A601D60E7BAF3F69F98077D /* Pods */ = {
isa = PBXGroup;
children = (
0B67A4E592FF13260AAFD656 /* Pods-Runner.debug.xcconfig */,
72900351EF1A3F028032459A /* Pods-Runner.release.xcconfig */,
3E54CF83D4EE560125987C8A /* Pods-Runner.profile.xcconfig */,
306514357AC94BE3DDEBC8D8 /* Pods-RunnerTests.debug.xcconfig */,
E328C7F89A365CDC0EAD15C6 /* Pods-RunnerTests.release.xcconfig */,
8215052AB7CBF47CD3DAAF69 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
9B0912232A54593A001108B7 /* ProxyPin */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
8A601D60E7BAF3F69F98077D /* Pods */,
28892733E959FF4F4696A049 /* Frameworks */,
);
sourceTree = "";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
9B0912202A54593A001108B7 /* ProxyPin.appex */,
);
name = Products;
sourceTree = "";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
9BE87B5A2DEA47DE00F4FCEF /* Handlers */,
9BC4B8D12B4C19ED0047DBDD /* pip */,
9B7077352A5728B900F184A9 /* silence.mp3 */,
9B09121A2A5457B3001108B7 /* VpnManager.swift */,
9B0912192A545757001108B7 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
9B70772C2A5718FB00F184A9 /* AudioManager.swift */,
9B90F5822C183CDE007D7A81 /* InfoPlist.strings */,
);
path = Runner;
sourceTree = "";
};
9B0912232A54593A001108B7 /* ProxyPin */ = {
isa = PBXGroup;
children = (
9BCA28642C97729000C2B46C /* vpn */,
9B0912242A54593A001108B7 /* PacketTunnelProvider.swift */,
9B0912262A54593A001108B7 /* Info.plist */,
9B0912272A54593A001108B7 /* ProxyPin.entitlements */,
9BAB4FC42DE75E9A0093BFBA /* ProxyPin-Bridging-Header.h */,
);
path = ProxyPin;
sourceTree = "";
};
9BAB4FB12DE74F570093BFBA /* ping */ = {
isa = PBXGroup;
children = (
9BAB4FC22DE75D220093BFBA /* GBPingHelper.swift */,
9BAB4FBB2DE75CFE0093BFBA /* GBPing.h */,
9BAB4FBC2DE75CFE0093BFBA /* GBPing.m */,
9BAB4FBD2DE75CFE0093BFBA /* GBPingSummary.h */,
9BAB4FBE2DE75CFE0093BFBA /* GBPingSummary.m */,
9BAB4FBF2DE75CFE0093BFBA /* ICMPHeader.h */,
);
path = ping;
sourceTree = "";
};
9BC4B8D12B4C19ED0047DBDD /* pip */ = {
isa = PBXGroup;
children = (
9B2A10C72B4CBE32001C443F /* silience.mov */,
9BC4B8CB2B4B48710047DBDD /* PictureInPictureManager.swift */,
9B2A10C52B4CA9A6001C443F /* PictureInPictureView.swift */,
);
path = pip;
sourceTree = "";
};
9BCA28642C97729000C2B46C /* vpn */ = {
isa = PBXGroup;
children = (
9BAB4FB12DE74F570093BFBA /* ping */,
9BCA287B2C989A8700C2B46C /* socket */,
9BCA28762C98901800C2B46C /* utils */,
9BCA28672C97746200C2B46C /* transport */,
9BCA28652C9772DD00C2B46C /* ConnectionHandler.swift */,
9BCA28702C987B0B00C2B46C /* ConnectionManager.swift */,
9BCA28792C989A7200C2B46C /* Connection.swift */,
9BCA287E2C989AF300C2B46C /* NWProtocol.swift */,
9BCA28822C98AA9000C2B46C /* ProxyVpnService.swift */,
9BCA28842C98C6B300C2B46C /* QueueFactory.swift */,
);
path = vpn;
sourceTree = "";
};
9BCA28672C97746200C2B46C /* transport */ = {
isa = PBXGroup;
children = (
9BCA28682C97747000C2B46C /* protocol */,
9BCA28722C988E9D00C2B46C /* Packet.swift */,
);
path = transport;
sourceTree = "";
};
9BCA28682C97747000C2B46C /* protocol */ = {
isa = PBXGroup;
children = (
9BCA28692C97748100C2B46C /* IP4Header.swift */,
9BCA286C2C977E3800C2B46C /* TCPHeader.swift */,
9BCA286E2C977E4C00C2B46C /* TCPPacketFactory.swift */,
9BCA28742C988EC400C2B46C /* TransportHeader.swift */,
9BCA288B2C995B3700C2B46C /* UDPHeader.swift */,
9B5125A92CAEE3350027996E /* ICMPPacket.swift */,
);
path = protocol;
sourceTree = "";
};
9BCA28762C98901800C2B46C /* utils */ = {
isa = PBXGroup;
children = (
9BE87B5D2DEA6BAB00F4FCEF /* TLS.swift */,
9BCA28772C98902900C2B46C /* PacketUtil.swift */,
);
path = utils;
sourceTree = "";
};
9BCA287B2C989A8700C2B46C /* socket */ = {
isa = PBXGroup;
children = (
9BCA287C2C989A9F00C2B46C /* CloseableConnection.swift */,
9BCA28802C98A42A00C2B46C /* ClientPacketWriter.swift */,
9BCA28892C98C82000C2B46C /* SocketIOService.swift */,
);
path = socket;
sourceTree = "";
};
9BE87B5A2DEA47DE00F4FCEF /* Handlers */ = {
isa = PBXGroup;
children = (
9BE87B5B2DEA47FA00F4FCEF /* MethodHandler.swift */,
);
path = Handlers;
sourceTree = "";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
E7E8C74F615A57D43D59596C /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
2C2BB3BDC059E8FD67F7FF64 /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
35A8CB519E229982B14B0197 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9B09122B2A54593B001108B7 /* Embed Foundation Extensions */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
593E01BCFF86ADFAC59E51D5 /* [CP] Embed Pods Frameworks */,
298BDEFE069E2E1C3876CA2D /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
9B0912292A54593A001108B7 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
9B09121F2A54593A001108B7 /* ProxyPin */ = {
isa = PBXNativeTarget;
buildConfigurationList = 9B09122F2A54593B001108B7 /* Build configuration list for PBXNativeTarget "ProxyPin" */;
buildPhases = (
9B09121C2A54593A001108B7 /* Sources */,
9B09121D2A54593A001108B7 /* Frameworks */,
9B09121E2A54593A001108B7 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = ProxyPin;
productName = ProxyPin;
productReference = 9B0912202A54593A001108B7 /* ProxyPin.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1420;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
9B09121F2A54593A001108B7 = {
CreatedOnToolsVersion = 14.2;
LastSwiftMigration = 1630;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 13.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
"zh-Hans",
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
9B09121F2A54593A001108B7 /* ProxyPin */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9B2A10C82B4CBE32001C443F /* silience.mov in Resources */,
9B90F5802C183CDE007D7A81 /* InfoPlist.strings in Resources */,
9B7077362A5728B900F184A9 /* silence.mp3 in Resources */,
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
9B09121E2A54593A001108B7 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
298BDEFE069E2E1C3876CA2D /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
35A8CB519E229982B14B0197 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n";
};
593E01BCFF86ADFAC59E51D5 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
E7E8C74F615A57D43D59596C /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9B2A10C62B4CA9A6001C443F /* PictureInPictureView.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
9B70772D2A5718FB00F184A9 /* AudioManager.swift in Sources */,
9BE87B5C2DEA480000F4FCEF /* MethodHandler.swift in Sources */,
9B09121B2A5457B3001108B7 /* VpnManager.swift in Sources */,
9BC4B8CC2B4B48710047DBDD /* PictureInPictureManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
9B09121C2A54593A001108B7 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9BCA28832C98AA9000C2B46C /* ProxyVpnService.swift in Sources */,
9BCA287D2C989A9F00C2B46C /* CloseableConnection.swift in Sources */,
9B5125AA2CAEE3350027996E /* ICMPPacket.swift in Sources */,
9BCA28662C9772DD00C2B46C /* ConnectionHandler.swift in Sources */,
9BCA28712C987B0C00C2B46C /* ConnectionManager.swift in Sources */,
9BCA28812C98A42A00C2B46C /* ClientPacketWriter.swift in Sources */,
9B0912252A54593A001108B7 /* PacketTunnelProvider.swift in Sources */,
9BCA286D2C977E3800C2B46C /* TCPHeader.swift in Sources */,
9BCA28782C98902900C2B46C /* PacketUtil.swift in Sources */,
9BCA288C2C995B3700C2B46C /* UDPHeader.swift in Sources */,
9BCA28732C988E9D00C2B46C /* Packet.swift in Sources */,
9BCA288A2C98C82000C2B46C /* SocketIOService.swift in Sources */,
9BAB4FC02DE75CFE0093BFBA /* GBPing.m in Sources */,
9BAB4FC12DE75CFE0093BFBA /* GBPingSummary.m in Sources */,
9BCA28852C98C6B300C2B46C /* QueueFactory.swift in Sources */,
9BCA286A2C97748100C2B46C /* IP4Header.swift in Sources */,
9BCA287A2C989A7200C2B46C /* Connection.swift in Sources */,
9BAB4FC32DE75D220093BFBA /* GBPingHelper.swift in Sources */,
9BCA28752C988EC400C2B46C /* TransportHeader.swift in Sources */,
9BCA287F2C989AF300C2B46C /* NWProtocol.swift in Sources */,
9BCA286F2C977E4C00C2B46C /* TCPPacketFactory.swift in Sources */,
9BE87B5E2DEA6BAE00F4FCEF /* TLS.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
9B0912292A54593A001108B7 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 9B09121F2A54593A001108B7 /* ProxyPin */;
targetProxy = 9B0912282A54593A001108B7 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
9B90F57C2C183C7E007D7A81 /* zh-Hans */,
);
name = Main.storyboard;
sourceTree = "";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
9B90F57D2C183C7E007D7A81 /* zh-Hans */,
);
name = LaunchScreen.storyboard;
sourceTree = "";
};
9B90F5822C183CDE007D7A81 /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
9B90F5812C183CDE007D7A81 /* en */,
9B90F5832C183CE0007D7A81 /* zh-Hans */,
);
name = InfoPlist.strings;
sourceTree = "";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = DM3F8VR243;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 306514357AC94BE3DDEBC8D8 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.networkProxyFlutter.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E328C7F89A365CDC0EAD15C6 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.networkProxyFlutter.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 8215052AB7CBF47CD3DAAF69 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.networkProxyFlutter.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = DM3F8VR243;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = DM3F8VR243;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
9B09122C2A54593B001108B7 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ProxyPin/ProxyPin.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = DM3F8VR243;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ProxyPin/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ProxyPin;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.ProxyPin;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "ProxyPin/ProxyPin-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
9B09122D2A54593B001108B7 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ProxyPin/ProxyPin.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = DM3F8VR243;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ProxyPin/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ProxyPin;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.ProxyPin;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "ProxyPin/ProxyPin-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
9B09122E2A54593B001108B7 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ProxyPin/ProxyPin.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = DM3F8VR243;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ProxyPin/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ProxyPin;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.proxy.pin.ProxyPin;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "ProxyPin/ProxyPin-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
9B09122F2A54593B001108B7 /* Build configuration list for PBXNativeTarget "ProxyPin" */ = {
isa = XCConfigurationList;
buildConfigurations = (
9B09122C2A54593B001108B7 /* Debug */,
9B09122D2A54593B001108B7 /* Release */,
9B09122E2A54593B001108B7 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
================================================
FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
IDEDidComputeMac32BitWarning
================================================
FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
================================================
PreviewsEnabled
================================================
FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
================================================
================================================
FILE: ios/Runner.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
IDEDidComputeMac32BitWarning
================================================
FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
================================================
PreviewsEnabled
================================================
FILE: ios/RunnerTests/RunnerTests.swift
================================================
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
================================================
FILE: l10n.yaml
================================================
#synthetic-package: false
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
untranslated-messages-file: l10n_errors.txt
================================================
FILE: lib/l10n/app_en.arb
================================================
{
"breakpoint": "Breakpoint",
"breakpointRule": "Breakpoint Rule",
"name": "Name",
"requests": "Requests",
"favorites": "Favorites",
"history": "History",
"toolbox": "Toolbox",
"preference": "Preferences",
"feedback": "Feedback",
"about": "About",
"filter": "Proxy Filter",
"script": "Script",
"share": "Share",
"port": "Port: ",
"proxy": "Proxy",
"externalProxy": "External Proxy",
"username": "Username",
"password": "Password",
"proxySetting": "Proxy Setting",
"setAs": "Set as ",
"systemProxy": "System Proxy",
"enabledHTTP2": "Enable HTTP2",
"serverNotStart": "Proxy server not started",
"download": "Download",
"config": "Configuration",
"version": "Version",
"start": "Start",
"stop": "Stop",
"clear": "Clear",
"httpsProxy": "HTTPS Proxy",
"setting": "Settings",
"mobileConnect": "Mobile Connect",
"connectRemote": "Connect Remote",
"remoteDevice": "Remote Device",
"remoteDeviceList": "Remote Device List",
"myQRCode": "My QR Code",
"theme": "Theme",
"followSystem": "Follow System",
"themeColor": "Theme Color",
"themeLight": "Light",
"themeDark": "Dark",
"language": "Language",
"autoStartup": "Auto Start Recording Traffic",
"autoStartupDescribe": "Automatically start recording traffic when the program starts",
"copied": "Copied to clipboard",
"execute": "Execute",
"cancel": "Cancel",
"close": "Close",
"save": "Save",
"confirm": "Confirm",
"confirmTitle": "Confirm operation",
"confirmContent": "Are you sure about this operation?",
"addSuccess": "Successfully added",
"saveSuccess": "Saved successfully",
"operationSuccess": "Operation succeeded",
"import": "Import",
"importSuccess": "Import successful",
"importFailed": "Import failed",
"export": "Export",
"exportSuccess": "Export successful",
"exportFailed": "Export failed",
"deleteSuccess": "Delete successful",
"send": "Send",
"fail": "fail",
"success": "success",
"emptyData": "Empty Data",
"requestSuccess": "Request successful",
"add": "Add",
"all": "All",
"modify": "Modify",
"responseType": "Response Type",
"request": "Request",
"response": "Response",
"statusCode": "Status code",
"duration": "Duration",
"done": "Done",
"type": "Type",
"enable": "Enable",
"example": "Example: ",
"responseHeader": "Headers",
"requestHeader": "Headers",
"requestLine": "Request Line",
"requestMethod": "Request Method",
"param": "Param",
"replaceBodyWith": "Replace Body With:",
"redirectTo": "Redirect To:",
"redirect": "Redirect",
"cannotBeEmpty": "Cannot be empty",
"requestRewriteList": "Request Rewrite List",
"requestRewriteRule": "Request Rewrite Rule",
"requestRewriteEnable": "Enable Request Rewrite",
"action": "Action",
"multiple": "Multiple",
"edit": "Edit",
"disabled": "Disabled",
"requestRewriteDeleteConfirm": "Delete {size} rule(s)?",
"useGuide": "Use Guide",
"pleaseEnter": "Please Enter",
"click": "Click",
"replace": "Replace",
"clickEdit": "Click Edit",
"refresh": "Refresh",
"selectFile": "Select file",
"match": "Match",
"value": "Value",
"matchRule": "Match Rule",
"emptyMatchAll": "Empty means match all",
"newBuilt": "New",
"reportServers": "Report Servers",
"addReportServer": "Add Report Server",
"editReportServer": "Edit Report Server",
"serverUrl": "Server URL",
"compression": "Compression",
"compressionNone": "None",
"newFolder": "New Folder",
"enableSelect": "Enable Select",
"disableSelect": "Disable Select",
"deleteSelect": "Delete Select",
"testData": "Test Data",
"noChangesDetected": "No changes detected",
"enterMatchData": "Enter the data to be matched",
"modifyRequestHeader": "Modify Header",
"headerName": "Header Name",
"headerValue": "Header Value",
"deleteHeaderConfirm": "Do you want to delete the request header?",
"sequence": "All Requests",
"domainList": "Domain List",
"domainWhitelist": "Proxy Domain Whitelist",
"domainBlacklist": "Proxy Domain Blacklist",
"domainFilter": "Proxy Domain List",
"appWhitelist": "App Whitelist",
"appWhitelistDescribe": "Only proxy Apps on the whitelist. If the whitelist is enabled, the blacklist will be invalid",
"appBlacklist": "App Blacklist",
"scanCode": "Scan Code Connect",
"addBlacklist": "Add Proxy Blacklist",
"addWhitelist": "Add Proxy Whitelist",
"deleteWhitelist": "Delete Proxy Whitelist",
"domainListSubtitle": "Last Request Time: {time}, Count: {count}",
"selectAction": "Select action",
"copy": "Copy",
"copyHost": "Copy Host",
"copyUrl": "Copy URL",
"copyRawRequest": "Copy Raw Request",
"copyRequestResponse": "Copy Request and Response",
"copyCurl": "Copy cURL",
"copyAsPythonRequests": "Copy as Python Requests",
"delete": "Delete",
"rename": "Rename",
"repeat": "Repeat",
"repeatAllRequests": "Repeat All Requests",
"repeatDomainRequests": "Repeat Domain Requests",
"customRepeat": "Custom Repeat",
"repeatCount": "Iterations",
"repeatInterval": "Interval(ms)",
"repeatDelay": "Delay(ms)",
"scheduleTime": "Schedule Time",
"fixed": "fixed",
"random": "random",
"keepCustomSettings": "Keep custom settings",
"editRequest": "Edit and Request",
"reSendRequest": "The request has been resent",
"viewExport": "View Export",
"timeDesc": "Descending by time",
"timeAsc": "Ascending by time",
"search": "Search",
"clearSearch": "Clear Search",
"requestType": "Request type",
"keyword": "Keyword",
"keywordSearchScope": "Keyword search scope: ",
"favorite": "Favorite",
"deleteFavorite": "Delete Favorite",
"emptyFavorite": "Empty Favorite",
"deleteFavoriteSuccess": "Favorite deleted",
"historyRecord": "History",
"historyCacheTime": "Cache Time",
"historyManualSave": "Manual Save",
"historyDay": "{day} days",
"historyForever": "Forever",
"historyRecordTitle": "{name} Records {length}",
"historyEmptyName": "Name cannot be empty",
"historySubtitle": "Records {requestLength} file {size}",
"historyUnSave": "Current record is not saved",
"historyDeleteConfirm": "Do you want to delete this history?",
"requestEdit": "Request Editing",
"encode": "Encode",
"requestBody": "Request Body",
"responseBody": "Response Body",
"requestRewrite": "Request Rewrite",
"newWindow": "New Window",
"httpRequest": "HTTP Request",
"enabledHttps": "Enable HTTPS Proxy",
"installRootCa": "Install Certificate",
"installCaLocal": "Install Certificate to Local-Machine",
"downloadRootCa": "Download Certificate",
"downloadRootCaNote": "Note: If you set the default browser to other than Safari, click this line to copy and paste the link to Safari browser",
"generateCA": "Generate new root certificate",
"generateCADescribe": "Are you sure you want to generate a new root certificate? If confirmed,\nYou need to reinstall and trust the new certificate",
"resetDefaultCA": "Reset Default Root Certificate",
"resetDefaultCADescribe": "Are you sure you want to reset to the default root certificate?\nProxyPin default root certificate is the same for all users.",
"exportCaP12": "Export Root Certificate(.p12)",
"importCaP12": "Import Root Certificate(.p12)",
"trustCa": "Trust Certificate",
"profileDownload": "Profile Download",
"exportCA": "Export Root Certificate",
"exportPrivateKey": "Export Private Key",
"install": "Install",
"installCaDescribe": "Install CA Setting > Profile Download > Install",
"trustCaDescribe": "Trust CA Setting > General > About > Certificate Trust Setting",
"androidRoot": "System Certificate (ROOT Device)",
"androidRootMagisk": "Magisk module: \nAndroid ROOT devices can be used Magisk ProxyPinCA System Certificate Module, After installing and restarting the phone Check the system certificate to see if there is a ProxyPinCA certificate. If there is, it indicates that the certificate has been successfully installed。",
"androidRootRename": "If the module does not take effect, you can install the system root certificate according to the online tutorial, and name the root certificate {name}",
"androidRootCADownload": "Download System Certificate(.0)",
"androidUserCA": "User Certificate",
"androidUserCATips": "Tips: Android7+ many apps will not trust user certificates",
"androidUserCAInstall": "Open settings -> Security -> Encryption and credentials -> Install certificate -> CA certificate",
"androidUserXposed": "It is recommended to use the Xposed module for packet capture (no need for ROOT), click to view wiki",
"configWifiProxy": "Configure mobile Wi-Fi proxy",
"caInstallGuide": "Certificate Installation Guide",
"caAndroidBrowser": "Open Google Browser on Android devices:",
"caIosBrowser": "Open Safari on iOS devices:",
"localIP": "Local IP ",
"mobileScan": "Configure Wi-Fi proxy or Scan with Mobile App",
"decode": "Decode",
"encodeInput": "Enter the content to be converted",
"encodeResult": "Conversion Result",
"encodeFail": "Encoding failed",
"decodeFail": "Decoding failed",
"shareUrl": "Share Request URL",
"shareCurl": "Share cURL Request",
"shareRequestResponse": "Share Request and Response",
"captureDetail": "Capture Detail",
"proxyPinSoftware": "ProxyPin Open source traffic capture software for all platforms",
"prompt": "Prompt",
"curlSchemeRequest": "If the curl format is recognized, should it be converted into an HTTP request?",
"appExitTips": "Press again to exit the program",
"remoteConnectDisconnect": "Check remote connection failed, disconnected",
"connect": "Connect",
"reconnect": "Reconnect",
"remoteConnected": "Connected {os}, traffic will be forwarded to {os}",
"remoteConnectForward": "Remote connection, forwarding requests to other terminals",
"connectSuccess": "Connect successful",
"connectedRemote": "Connected to remote",
"connected": "Connected",
"notConnected": "Not connected",
"disconnect": "Disconnect",
"ipLayerProxy": "IP Layer Proxy(Beta)",
"ipLayerProxyDesc": "IP layer proxy can capture Flutter app requests, currently not very stable, welcome to submit PR",
"inputAddress": "Input Address",
"syncConfig": "Sync configuration",
"pullConfigFail": "Failed to pull configuration, please check the network connection",
"sync": "Sync",
"invalidQRCode": "Unrecognized QR code",
"remoteConnectFail": "Connection failed,Please check if it is allowed on the same LAN and firewall, iOS needs to enable local network permissions",
"remoteConnectSuccessTips": "Your phone needs to enable packet capture in order to capture requests",
"windowMode": "Window Mode",
"windowModeSubTitle": "Enabled Packet Capture, Enter the background, Display a small window",
"pipIcon": "Window shortcut icon",
"pipIconDescribe": "Show quick access to small window Icon",
"headerExpanded": "Headers Expanded",
"headerExpandedSubtitle": "Details page Headers is expanded by default",
"bottomNavigation": "Bottom Navigation",
"bottomNavigationSubtitle": "Bottom navigation bar is displayed, effective after restart",
"memoryCleanup": "Memory Cleanup",
"memoryCleanupSubtitle": "Automatically clean up requests on memory limit reached and keep 32 most recent after cleaning",
"unlimited": "Unlimited",
"custom": "Custom",
"externalProxyAuth": "Proxy Auth (Optional)",
"externalProxyServer": "Proxy Server",
"externalProxyConnectFailure": "External Proxy Connect failure",
"externalProxyFailureConfirm": "Access to all http will fail due to network connectivity issues,Do you want to continue setting up external proxies。",
"mobileDisplayPacketCapture": "Mobile Display Packet Capture:",
"proxyPortRepeat": "Startup failed, please check the port number {port} is occupied。",
"reset": "Reset",
"proxyIgnoreDomain": "Proxy ignores domain",
"domainWhitelistDescribe": "Only proxy domain names on the whitelist. If the whitelist is enabled, the blacklist will be invalid",
"domainBlacklistDescribe": "Domain names on the blacklist will not be proxied",
"domain": "Host",
"enableScript": "Enable Script",
"scriptUseDescribe": "Use JavaScript to modify requests and responses",
"scriptEdit": "Edit script",
"scrollEnd": "Scroll to End",
"logger": "Log",
"material3": "Material 3 is the latest version of Google’s open-source design system",
"iosVpnBackgroundAudio": "After turning on packet capture, exit to the background. In order to maintain the main UI thread for network communication, a silent audio playback will be enabled to keep the main thread running. Otherwise, it will only run in the background for 30 seconds. Do you agree to play audio in the background after turning on packet capture?",
"markRead": "Mark as read",
"autoRead": "Auto read",
"highlight": "Highlight",
"blue" : "Blue",
"green" : "Green",
"yellow" : "Yellow",
"red" : "Red",
"pink" : "Pink",
"gray" : "Gray",
"underline" : "Underline",
"requestBlock": "Request Block",
"other": "Other",
"certHashName": "CA Hash Name",
"regExp": "RegExp",
"systemCertName": "System Certificate Name",
"qrCode": "QR Code",
"scanQrCode": "Scan QR Code",
"generateQrCode": "Generate",
"saveImage": "Save Image",
"selectImage": "Select Image",
"inputContent": "Input Content",
"errorCorrectLevel": "Error Correct",
"output": "Output",
"timestamp": "Timestamp",
"convert": "Convert",
"time": "DateTime",
"nowTimestamp": "Now timestamp",
"hosts": "Hosts",
"toAddress": "To Address",
"encrypt": "Encrypt",
"decrypt": "Decrypt",
"cipher": "Cipher",
"appUpdateCheckVersion": "Check for Updates",
"appUpdateNotAvailableMsg": "Already Using The Latest Version",
"appUpdateDialogTitle": "Update Available",
"appUpdateUpdateMsg": "A new version of ProxyPin is available. Would you like to update now?",
"appUpdateCurrentVersionLbl": "Current Version",
"appUpdateNewVersionLbl": "New Version",
"appUpdateUpdateNowBtnTxt": "Update Now",
"appUpdateLaterBtnTxt": "Later",
"appUpdateIgnoreBtnTxt": "Ignore",
"requestMap": "Request Map",
"requestMapDescribe": "Do not request remote services, use local configuration or script for response",
"automatic": "Automatic",
"manual": "Manual",
"certNotInstalled": "Certificate not installed",
"openNewWindow": "Open New Window",
"sponsorDonate": "Sponsor / Donate",
"sponsorSupport": "Support ongoing development",
"sponsorThanks": "Thank you for supporting this open-source project by choosing any of the following methods to help its long-term development.",
"sponsorAfdian": "AFDIAN",
"sponsorBuyMeCoffee": "Buy Me a Coffee",
"privacyPolicy": "Privacy Policy",
"privacyContent": "This open-source packet capture tool runs entirely on your device. It has no backend server and does not collect, store, or upload any personal data. All captured traffic is processed locally and is only forwarded when you explicitly use remote forwarding. Permissions (e.g., network, storage, and camera for QR codes) are used solely to provide features. You can audit the behavior in the public source code.",
"requestCrypto": "Request Crypto",
"cryptoDecoded": "Decoded",
"cryptoDecodeToggle": "Decrypt",
"optional": "Optional",
"cryptoRuleField": "Field Name",
"cryptoIvPrefixLabel": "IV Prefix",
"cryptoIvPrefixTooltip": "Use the first N bytes of the response body as IV",
"local": "Local",
"remoteUrl": "Remote URL",
"view": "View"
}
================================================
FILE: lib/l10n/app_localizations.dart
================================================
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_en.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, you’ll need to edit this
/// file.
///
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// project’s Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of(context, AppLocalizations);
}
static const LocalizationsDelegate delegate = _AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List> localizationsDelegates = >[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List supportedLocales = [
Locale('en'),
Locale('zh'),
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant')
];
/// No description provided for @breakpoint.
///
/// In en, this message translates to:
/// **'Breakpoint'**
String get breakpoint;
/// No description provided for @breakpointRule.
///
/// In en, this message translates to:
/// **'Breakpoint Rule'**
String get breakpointRule;
/// No description provided for @name.
///
/// In en, this message translates to:
/// **'Name'**
String get name;
/// No description provided for @requests.
///
/// In en, this message translates to:
/// **'Requests'**
String get requests;
/// No description provided for @favorites.
///
/// In en, this message translates to:
/// **'Favorites'**
String get favorites;
/// No description provided for @history.
///
/// In en, this message translates to:
/// **'History'**
String get history;
/// No description provided for @toolbox.
///
/// In en, this message translates to:
/// **'Toolbox'**
String get toolbox;
/// No description provided for @preference.
///
/// In en, this message translates to:
/// **'Preferences'**
String get preference;
/// No description provided for @feedback.
///
/// In en, this message translates to:
/// **'Feedback'**
String get feedback;
/// No description provided for @about.
///
/// In en, this message translates to:
/// **'About'**
String get about;
/// No description provided for @filter.
///
/// In en, this message translates to:
/// **'Proxy Filter'**
String get filter;
/// No description provided for @script.
///
/// In en, this message translates to:
/// **'Script'**
String get script;
/// No description provided for @share.
///
/// In en, this message translates to:
/// **'Share'**
String get share;
/// No description provided for @port.
///
/// In en, this message translates to:
/// **'Port: '**
String get port;
/// No description provided for @proxy.
///
/// In en, this message translates to:
/// **'Proxy'**
String get proxy;
/// No description provided for @externalProxy.
///
/// In en, this message translates to:
/// **'External Proxy'**
String get externalProxy;
/// No description provided for @username.
///
/// In en, this message translates to:
/// **'Username'**
String get username;
/// No description provided for @password.
///
/// In en, this message translates to:
/// **'Password'**
String get password;
/// No description provided for @proxySetting.
///
/// In en, this message translates to:
/// **'Proxy Setting'**
String get proxySetting;
/// No description provided for @setAs.
///
/// In en, this message translates to:
/// **'Set as '**
String get setAs;
/// No description provided for @systemProxy.
///
/// In en, this message translates to:
/// **'System Proxy'**
String get systemProxy;
/// No description provided for @enabledHTTP2.
///
/// In en, this message translates to:
/// **'Enable HTTP2'**
String get enabledHTTP2;
/// No description provided for @serverNotStart.
///
/// In en, this message translates to:
/// **'Proxy server not started'**
String get serverNotStart;
/// No description provided for @download.
///
/// In en, this message translates to:
/// **'Download'**
String get download;
/// No description provided for @config.
///
/// In en, this message translates to:
/// **'Configuration'**
String get config;
/// No description provided for @version.
///
/// In en, this message translates to:
/// **'Version'**
String get version;
/// No description provided for @start.
///
/// In en, this message translates to:
/// **'Start'**
String get start;
/// No description provided for @stop.
///
/// In en, this message translates to:
/// **'Stop'**
String get stop;
/// No description provided for @clear.
///
/// In en, this message translates to:
/// **'Clear'**
String get clear;
/// No description provided for @httpsProxy.
///
/// In en, this message translates to:
/// **'HTTPS Proxy'**
String get httpsProxy;
/// No description provided for @setting.
///
/// In en, this message translates to:
/// **'Settings'**
String get setting;
/// No description provided for @mobileConnect.
///
/// In en, this message translates to:
/// **'Mobile Connect'**
String get mobileConnect;
/// No description provided for @connectRemote.
///
/// In en, this message translates to:
/// **'Connect Remote'**
String get connectRemote;
/// No description provided for @remoteDevice.
///
/// In en, this message translates to:
/// **'Remote Device'**
String get remoteDevice;
/// No description provided for @remoteDeviceList.
///
/// In en, this message translates to:
/// **'Remote Device List'**
String get remoteDeviceList;
/// No description provided for @myQRCode.
///
/// In en, this message translates to:
/// **'My QR Code'**
String get myQRCode;
/// No description provided for @theme.
///
/// In en, this message translates to:
/// **'Theme'**
String get theme;
/// No description provided for @followSystem.
///
/// In en, this message translates to:
/// **'Follow System'**
String get followSystem;
/// No description provided for @themeColor.
///
/// In en, this message translates to:
/// **'Theme Color'**
String get themeColor;
/// No description provided for @themeLight.
///
/// In en, this message translates to:
/// **'Light'**
String get themeLight;
/// No description provided for @themeDark.
///
/// In en, this message translates to:
/// **'Dark'**
String get themeDark;
/// No description provided for @language.
///
/// In en, this message translates to:
/// **'Language'**
String get language;
/// No description provided for @autoStartup.
///
/// In en, this message translates to:
/// **'Auto Start Recording Traffic'**
String get autoStartup;
/// No description provided for @autoStartupDescribe.
///
/// In en, this message translates to:
/// **'Automatically start recording traffic when the program starts'**
String get autoStartupDescribe;
/// No description provided for @copied.
///
/// In en, this message translates to:
/// **'Copied to clipboard'**
String get copied;
/// No description provided for @execute.
///
/// In en, this message translates to:
/// **'Execute'**
String get execute;
/// No description provided for @cancel.
///
/// In en, this message translates to:
/// **'Cancel'**
String get cancel;
/// No description provided for @close.
///
/// In en, this message translates to:
/// **'Close'**
String get close;
/// No description provided for @save.
///
/// In en, this message translates to:
/// **'Save'**
String get save;
/// No description provided for @confirm.
///
/// In en, this message translates to:
/// **'Confirm'**
String get confirm;
/// No description provided for @confirmTitle.
///
/// In en, this message translates to:
/// **'Confirm operation'**
String get confirmTitle;
/// No description provided for @confirmContent.
///
/// In en, this message translates to:
/// **'Are you sure about this operation?'**
String get confirmContent;
/// No description provided for @addSuccess.
///
/// In en, this message translates to:
/// **'Successfully added'**
String get addSuccess;
/// No description provided for @saveSuccess.
///
/// In en, this message translates to:
/// **'Saved successfully'**
String get saveSuccess;
/// No description provided for @operationSuccess.
///
/// In en, this message translates to:
/// **'Operation succeeded'**
String get operationSuccess;
/// No description provided for @import.
///
/// In en, this message translates to:
/// **'Import'**
String get import;
/// No description provided for @importSuccess.
///
/// In en, this message translates to:
/// **'Import successful'**
String get importSuccess;
/// No description provided for @importFailed.
///
/// In en, this message translates to:
/// **'Import failed'**
String get importFailed;
/// No description provided for @export.
///
/// In en, this message translates to:
/// **'Export'**
String get export;
/// No description provided for @exportSuccess.
///
/// In en, this message translates to:
/// **'Export successful'**
String get exportSuccess;
/// No description provided for @exportFailed.
///
/// In en, this message translates to:
/// **'Export failed'**
String get exportFailed;
/// No description provided for @deleteSuccess.
///
/// In en, this message translates to:
/// **'Delete successful'**
String get deleteSuccess;
/// No description provided for @send.
///
/// In en, this message translates to:
/// **'Send'**
String get send;
/// No description provided for @fail.
///
/// In en, this message translates to:
/// **'fail'**
String get fail;
/// No description provided for @success.
///
/// In en, this message translates to:
/// **'success'**
String get success;
/// No description provided for @emptyData.
///
/// In en, this message translates to:
/// **'Empty Data'**
String get emptyData;
/// No description provided for @requestSuccess.
///
/// In en, this message translates to:
/// **'Request successful'**
String get requestSuccess;
/// No description provided for @add.
///
/// In en, this message translates to:
/// **'Add'**
String get add;
/// No description provided for @all.
///
/// In en, this message translates to:
/// **'All'**
String get all;
/// No description provided for @modify.
///
/// In en, this message translates to:
/// **'Modify'**
String get modify;
/// No description provided for @responseType.
///
/// In en, this message translates to:
/// **'Response Type'**
String get responseType;
/// No description provided for @request.
///
/// In en, this message translates to:
/// **'Request'**
String get request;
/// No description provided for @response.
///
/// In en, this message translates to:
/// **'Response'**
String get response;
/// No description provided for @statusCode.
///
/// In en, this message translates to:
/// **'Status code'**
String get statusCode;
/// No description provided for @duration.
///
/// In en, this message translates to:
/// **'Duration'**
String get duration;
/// No description provided for @done.
///
/// In en, this message translates to:
/// **'Done'**
String get done;
/// No description provided for @type.
///
/// In en, this message translates to:
/// **'Type'**
String get type;
/// No description provided for @enable.
///
/// In en, this message translates to:
/// **'Enable'**
String get enable;
/// No description provided for @example.
///
/// In en, this message translates to:
/// **'Example: '**
String get example;
/// No description provided for @responseHeader.
///
/// In en, this message translates to:
/// **'Headers'**
String get responseHeader;
/// No description provided for @requestHeader.
///
/// In en, this message translates to:
/// **'Headers'**
String get requestHeader;
/// No description provided for @requestLine.
///
/// In en, this message translates to:
/// **'Request Line'**
String get requestLine;
/// No description provided for @requestMethod.
///
/// In en, this message translates to:
/// **'Request Method'**
String get requestMethod;
/// No description provided for @param.
///
/// In en, this message translates to:
/// **'Param'**
String get param;
/// No description provided for @replaceBodyWith.
///
/// In en, this message translates to:
/// **'Replace Body With:'**
String get replaceBodyWith;
/// No description provided for @redirectTo.
///
/// In en, this message translates to:
/// **'Redirect To:'**
String get redirectTo;
/// No description provided for @redirect.
///
/// In en, this message translates to:
/// **'Redirect'**
String get redirect;
/// No description provided for @cannotBeEmpty.
///
/// In en, this message translates to:
/// **'Cannot be empty'**
String get cannotBeEmpty;
/// No description provided for @requestRewriteList.
///
/// In en, this message translates to:
/// **'Request Rewrite List'**
String get requestRewriteList;
/// No description provided for @requestRewriteRule.
///
/// In en, this message translates to:
/// **'Request Rewrite Rule'**
String get requestRewriteRule;
/// No description provided for @requestRewriteEnable.
///
/// In en, this message translates to:
/// **'Enable Request Rewrite'**
String get requestRewriteEnable;
/// No description provided for @action.
///
/// In en, this message translates to:
/// **'Action'**
String get action;
/// No description provided for @multiple.
///
/// In en, this message translates to:
/// **'Multiple'**
String get multiple;
/// No description provided for @edit.
///
/// In en, this message translates to:
/// **'Edit'**
String get edit;
/// No description provided for @disabled.
///
/// In en, this message translates to:
/// **'Disabled'**
String get disabled;
/// No description provided for @requestRewriteDeleteConfirm.
///
/// In en, this message translates to:
/// **'Delete {size} rule(s)?'**
String requestRewriteDeleteConfirm(Object size);
/// No description provided for @useGuide.
///
/// In en, this message translates to:
/// **'Use Guide'**
String get useGuide;
/// No description provided for @pleaseEnter.
///
/// In en, this message translates to:
/// **'Please Enter'**
String get pleaseEnter;
/// No description provided for @click.
///
/// In en, this message translates to:
/// **'Click'**
String get click;
/// No description provided for @replace.
///
/// In en, this message translates to:
/// **'Replace'**
String get replace;
/// No description provided for @clickEdit.
///
/// In en, this message translates to:
/// **'Click Edit'**
String get clickEdit;
/// No description provided for @refresh.
///
/// In en, this message translates to:
/// **'Refresh'**
String get refresh;
/// No description provided for @selectFile.
///
/// In en, this message translates to:
/// **'Select file'**
String get selectFile;
/// No description provided for @match.
///
/// In en, this message translates to:
/// **'Match'**
String get match;
/// No description provided for @value.
///
/// In en, this message translates to:
/// **'Value'**
String get value;
/// No description provided for @matchRule.
///
/// In en, this message translates to:
/// **'Match Rule'**
String get matchRule;
/// No description provided for @emptyMatchAll.
///
/// In en, this message translates to:
/// **'Empty means match all'**
String get emptyMatchAll;
/// No description provided for @newBuilt.
///
/// In en, this message translates to:
/// **'New'**
String get newBuilt;
/// No description provided for @reportServers.
///
/// In en, this message translates to:
/// **'Report Servers'**
String get reportServers;
/// No description provided for @addReportServer.
///
/// In en, this message translates to:
/// **'Add Report Server'**
String get addReportServer;
/// No description provided for @editReportServer.
///
/// In en, this message translates to:
/// **'Edit Report Server'**
String get editReportServer;
/// No description provided for @serverUrl.
///
/// In en, this message translates to:
/// **'Server URL'**
String get serverUrl;
/// No description provided for @compression.
///
/// In en, this message translates to:
/// **'Compression'**
String get compression;
/// No description provided for @compressionNone.
///
/// In en, this message translates to:
/// **'None'**
String get compressionNone;
/// No description provided for @newFolder.
///
/// In en, this message translates to:
/// **'New Folder'**
String get newFolder;
/// No description provided for @enableSelect.
///
/// In en, this message translates to:
/// **'Enable Select'**
String get enableSelect;
/// No description provided for @disableSelect.
///
/// In en, this message translates to:
/// **'Disable Select'**
String get disableSelect;
/// No description provided for @deleteSelect.
///
/// In en, this message translates to:
/// **'Delete Select'**
String get deleteSelect;
/// No description provided for @testData.
///
/// In en, this message translates to:
/// **'Test Data'**
String get testData;
/// No description provided for @noChangesDetected.
///
/// In en, this message translates to:
/// **'No changes detected'**
String get noChangesDetected;
/// No description provided for @enterMatchData.
///
/// In en, this message translates to:
/// **'Enter the data to be matched'**
String get enterMatchData;
/// No description provided for @modifyRequestHeader.
///
/// In en, this message translates to:
/// **'Modify Header'**
String get modifyRequestHeader;
/// No description provided for @headerName.
///
/// In en, this message translates to:
/// **'Header Name'**
String get headerName;
/// No description provided for @headerValue.
///
/// In en, this message translates to:
/// **'Header Value'**
String get headerValue;
/// No description provided for @deleteHeaderConfirm.
///
/// In en, this message translates to:
/// **'Do you want to delete the request header?'**
String get deleteHeaderConfirm;
/// No description provided for @sequence.
///
/// In en, this message translates to:
/// **'All Requests'**
String get sequence;
/// No description provided for @domainList.
///
/// In en, this message translates to:
/// **'Domain List'**
String get domainList;
/// No description provided for @domainWhitelist.
///
/// In en, this message translates to:
/// **'Proxy Domain Whitelist'**
String get domainWhitelist;
/// No description provided for @domainBlacklist.
///
/// In en, this message translates to:
/// **'Proxy Domain Blacklist'**
String get domainBlacklist;
/// No description provided for @domainFilter.
///
/// In en, this message translates to:
/// **'Proxy Domain List'**
String get domainFilter;
/// No description provided for @appWhitelist.
///
/// In en, this message translates to:
/// **'App Whitelist'**
String get appWhitelist;
/// No description provided for @appWhitelistDescribe.
///
/// In en, this message translates to:
/// **'Only proxy Apps on the whitelist. If the whitelist is enabled, the blacklist will be invalid'**
String get appWhitelistDescribe;
/// No description provided for @appBlacklist.
///
/// In en, this message translates to:
/// **'App Blacklist'**
String get appBlacklist;
/// No description provided for @scanCode.
///
/// In en, this message translates to:
/// **'Scan Code Connect'**
String get scanCode;
/// No description provided for @addBlacklist.
///
/// In en, this message translates to:
/// **'Add Proxy Blacklist'**
String get addBlacklist;
/// No description provided for @addWhitelist.
///
/// In en, this message translates to:
/// **'Add Proxy Whitelist'**
String get addWhitelist;
/// No description provided for @deleteWhitelist.
///
/// In en, this message translates to:
/// **'Delete Proxy Whitelist'**
String get deleteWhitelist;
/// No description provided for @domainListSubtitle.
///
/// In en, this message translates to:
/// **'Last Request Time: {time}, Count: {count}'**
String domainListSubtitle(Object count, Object time);
/// No description provided for @selectAction.
///
/// In en, this message translates to:
/// **'Select action'**
String get selectAction;
/// No description provided for @copy.
///
/// In en, this message translates to:
/// **'Copy'**
String get copy;
/// No description provided for @copyHost.
///
/// In en, this message translates to:
/// **'Copy Host'**
String get copyHost;
/// No description provided for @copyUrl.
///
/// In en, this message translates to:
/// **'Copy URL'**
String get copyUrl;
/// No description provided for @copyRawRequest.
///
/// In en, this message translates to:
/// **'Copy Raw Request'**
String get copyRawRequest;
/// No description provided for @copyRequestResponse.
///
/// In en, this message translates to:
/// **'Copy Request and Response'**
String get copyRequestResponse;
/// No description provided for @copyCurl.
///
/// In en, this message translates to:
/// **'Copy cURL'**
String get copyCurl;
/// No description provided for @copyAsPythonRequests.
///
/// In en, this message translates to:
/// **'Copy as Python Requests'**
String get copyAsPythonRequests;
/// No description provided for @delete.
///
/// In en, this message translates to:
/// **'Delete'**
String get delete;
/// No description provided for @rename.
///
/// In en, this message translates to:
/// **'Rename'**
String get rename;
/// No description provided for @repeat.
///
/// In en, this message translates to:
/// **'Repeat'**
String get repeat;
/// No description provided for @repeatAllRequests.
///
/// In en, this message translates to:
/// **'Repeat All Requests'**
String get repeatAllRequests;
/// No description provided for @repeatDomainRequests.
///
/// In en, this message translates to:
/// **'Repeat Domain Requests'**
String get repeatDomainRequests;
/// No description provided for @customRepeat.
///
/// In en, this message translates to:
/// **'Custom Repeat'**
String get customRepeat;
/// No description provided for @repeatCount.
///
/// In en, this message translates to:
/// **'Iterations'**
String get repeatCount;
/// No description provided for @repeatInterval.
///
/// In en, this message translates to:
/// **'Interval(ms)'**
String get repeatInterval;
/// No description provided for @repeatDelay.
///
/// In en, this message translates to:
/// **'Delay(ms)'**
String get repeatDelay;
/// No description provided for @scheduleTime.
///
/// In en, this message translates to:
/// **'Schedule Time'**
String get scheduleTime;
/// No description provided for @fixed.
///
/// In en, this message translates to:
/// **'fixed'**
String get fixed;
/// No description provided for @random.
///
/// In en, this message translates to:
/// **'random'**
String get random;
/// No description provided for @keepCustomSettings.
///
/// In en, this message translates to:
/// **'Keep custom settings'**
String get keepCustomSettings;
/// No description provided for @editRequest.
///
/// In en, this message translates to:
/// **'Edit and Request'**
String get editRequest;
/// No description provided for @reSendRequest.
///
/// In en, this message translates to:
/// **'The request has been resent'**
String get reSendRequest;
/// No description provided for @viewExport.
///
/// In en, this message translates to:
/// **'View Export'**
String get viewExport;
/// No description provided for @timeDesc.
///
/// In en, this message translates to:
/// **'Descending by time'**
String get timeDesc;
/// No description provided for @timeAsc.
///
/// In en, this message translates to:
/// **'Ascending by time'**
String get timeAsc;
/// No description provided for @search.
///
/// In en, this message translates to:
/// **'Search'**
String get search;
/// No description provided for @clearSearch.
///
/// In en, this message translates to:
/// **'Clear Search'**
String get clearSearch;
/// No description provided for @requestType.
///
/// In en, this message translates to:
/// **'Request type'**
String get requestType;
/// No description provided for @keyword.
///
/// In en, this message translates to:
/// **'Keyword'**
String get keyword;
/// No description provided for @keywordSearchScope.
///
/// In en, this message translates to:
/// **'Keyword search scope: '**
String get keywordSearchScope;
/// No description provided for @favorite.
///
/// In en, this message translates to:
/// **'Favorite'**
String get favorite;
/// No description provided for @deleteFavorite.
///
/// In en, this message translates to:
/// **'Delete Favorite'**
String get deleteFavorite;
/// No description provided for @emptyFavorite.
///
/// In en, this message translates to:
/// **'Empty Favorite'**
String get emptyFavorite;
/// No description provided for @deleteFavoriteSuccess.
///
/// In en, this message translates to:
/// **'Favorite deleted'**
String get deleteFavoriteSuccess;
/// No description provided for @historyRecord.
///
/// In en, this message translates to:
/// **'History'**
String get historyRecord;
/// No description provided for @historyCacheTime.
///
/// In en, this message translates to:
/// **'Cache Time'**
String get historyCacheTime;
/// No description provided for @historyManualSave.
///
/// In en, this message translates to:
/// **'Manual Save'**
String get historyManualSave;
/// No description provided for @historyDay.
///
/// In en, this message translates to:
/// **'{day} days'**
String historyDay(Object day);
/// No description provided for @historyForever.
///
/// In en, this message translates to:
/// **'Forever'**
String get historyForever;
/// No description provided for @historyRecordTitle.
///
/// In en, this message translates to:
/// **'{name} Records {length}'**
String historyRecordTitle(Object length, Object name);
/// No description provided for @historyEmptyName.
///
/// In en, this message translates to:
/// **'Name cannot be empty'**
String get historyEmptyName;
/// No description provided for @historySubtitle.
///
/// In en, this message translates to:
/// **'Records {requestLength} file {size}'**
String historySubtitle(Object requestLength, Object size);
/// No description provided for @historyUnSave.
///
/// In en, this message translates to:
/// **'Current record is not saved'**
String get historyUnSave;
/// No description provided for @historyDeleteConfirm.
///
/// In en, this message translates to:
/// **'Do you want to delete this history?'**
String get historyDeleteConfirm;
/// No description provided for @requestEdit.
///
/// In en, this message translates to:
/// **'Request Editing'**
String get requestEdit;
/// No description provided for @encode.
///
/// In en, this message translates to:
/// **'Encode'**
String get encode;
/// No description provided for @requestBody.
///
/// In en, this message translates to:
/// **'Request Body'**
String get requestBody;
/// No description provided for @responseBody.
///
/// In en, this message translates to:
/// **'Response Body'**
String get responseBody;
/// No description provided for @requestRewrite.
///
/// In en, this message translates to:
/// **'Request Rewrite'**
String get requestRewrite;
/// No description provided for @newWindow.
///
/// In en, this message translates to:
/// **'New Window'**
String get newWindow;
/// No description provided for @httpRequest.
///
/// In en, this message translates to:
/// **'HTTP Request'**
String get httpRequest;
/// No description provided for @enabledHttps.
///
/// In en, this message translates to:
/// **'Enable HTTPS Proxy'**
String get enabledHttps;
/// No description provided for @installRootCa.
///
/// In en, this message translates to:
/// **'Install Certificate'**
String get installRootCa;
/// No description provided for @installCaLocal.
///
/// In en, this message translates to:
/// **'Install Certificate to Local-Machine'**
String get installCaLocal;
/// No description provided for @downloadRootCa.
///
/// In en, this message translates to:
/// **'Download Certificate'**
String get downloadRootCa;
/// No description provided for @downloadRootCaNote.
///
/// In en, this message translates to:
/// **'Note: If you set the default browser to other than Safari, click this line to copy and paste the link to Safari browser'**
String get downloadRootCaNote;
/// No description provided for @generateCA.
///
/// In en, this message translates to:
/// **'Generate new root certificate'**
String get generateCA;
/// No description provided for @generateCADescribe.
///
/// In en, this message translates to:
/// **'Are you sure you want to generate a new root certificate? If confirmed,\nYou need to reinstall and trust the new certificate'**
String get generateCADescribe;
/// No description provided for @resetDefaultCA.
///
/// In en, this message translates to:
/// **'Reset Default Root Certificate'**
String get resetDefaultCA;
/// No description provided for @resetDefaultCADescribe.
///
/// In en, this message translates to:
/// **'Are you sure you want to reset to the default root certificate?\nProxyPin default root certificate is the same for all users.'**
String get resetDefaultCADescribe;
/// No description provided for @exportCaP12.
///
/// In en, this message translates to:
/// **'Export Root Certificate(.p12)'**
String get exportCaP12;
/// No description provided for @importCaP12.
///
/// In en, this message translates to:
/// **'Import Root Certificate(.p12)'**
String get importCaP12;
/// No description provided for @trustCa.
///
/// In en, this message translates to:
/// **'Trust Certificate'**
String get trustCa;
/// No description provided for @profileDownload.
///
/// In en, this message translates to:
/// **'Profile Download'**
String get profileDownload;
/// No description provided for @exportCA.
///
/// In en, this message translates to:
/// **'Export Root Certificate'**
String get exportCA;
/// No description provided for @exportPrivateKey.
///
/// In en, this message translates to:
/// **'Export Private Key'**
String get exportPrivateKey;
/// No description provided for @install.
///
/// In en, this message translates to:
/// **'Install'**
String get install;
/// No description provided for @installCaDescribe.
///
/// In en, this message translates to:
/// **'Install CA Setting > Profile Download > Install'**
String get installCaDescribe;
/// No description provided for @trustCaDescribe.
///
/// In en, this message translates to:
/// **'Trust CA Setting > General > About > Certificate Trust Setting'**
String get trustCaDescribe;
/// No description provided for @androidRoot.
///
/// In en, this message translates to:
/// **'System Certificate (ROOT Device)'**
String get androidRoot;
/// No description provided for @androidRootMagisk.
///
/// In en, this message translates to:
/// **'Magisk module: \nAndroid ROOT devices can be used Magisk ProxyPinCA System Certificate Module, After installing and restarting the phone Check the system certificate to see if there is a ProxyPinCA certificate. If there is, it indicates that the certificate has been successfully installed。'**
String get androidRootMagisk;
/// No description provided for @androidRootRename.
///
/// In en, this message translates to:
/// **'If the module does not take effect, you can install the system root certificate according to the online tutorial, and name the root certificate {name}'**
String androidRootRename(Object name);
/// No description provided for @androidRootCADownload.
///
/// In en, this message translates to:
/// **'Download System Certificate(.0)'**
String get androidRootCADownload;
/// No description provided for @androidUserCA.
///
/// In en, this message translates to:
/// **'User Certificate'**
String get androidUserCA;
/// No description provided for @androidUserCATips.
///
/// In en, this message translates to:
/// **'Tips: Android7+ many apps will not trust user certificates'**
String get androidUserCATips;
/// No description provided for @androidUserCAInstall.
///
/// In en, this message translates to:
/// **'Open settings -> Security -> Encryption and credentials -> Install certificate -> CA certificate'**
String get androidUserCAInstall;
/// No description provided for @androidUserXposed.
///
/// In en, this message translates to:
/// **'It is recommended to use the Xposed module for packet capture (no need for ROOT), click to view wiki'**
String get androidUserXposed;
/// No description provided for @configWifiProxy.
///
/// In en, this message translates to:
/// **'Configure mobile Wi-Fi proxy'**
String get configWifiProxy;
/// No description provided for @caInstallGuide.
///
/// In en, this message translates to:
/// **'Certificate Installation Guide'**
String get caInstallGuide;
/// No description provided for @caAndroidBrowser.
///
/// In en, this message translates to:
/// **'Open Google Browser on Android devices:'**
String get caAndroidBrowser;
/// No description provided for @caIosBrowser.
///
/// In en, this message translates to:
/// **'Open Safari on iOS devices:'**
String get caIosBrowser;
/// No description provided for @localIP.
///
/// In en, this message translates to:
/// **'Local IP '**
String get localIP;
/// No description provided for @mobileScan.
///
/// In en, this message translates to:
/// **'Configure Wi-Fi proxy or Scan with Mobile App'**
String get mobileScan;
/// No description provided for @decode.
///
/// In en, this message translates to:
/// **'Decode'**
String get decode;
/// No description provided for @encodeInput.
///
/// In en, this message translates to:
/// **'Enter the content to be converted'**
String get encodeInput;
/// No description provided for @encodeResult.
///
/// In en, this message translates to:
/// **'Conversion Result'**
String get encodeResult;
/// No description provided for @encodeFail.
///
/// In en, this message translates to:
/// **'Encoding failed'**
String get encodeFail;
/// No description provided for @decodeFail.
///
/// In en, this message translates to:
/// **'Decoding failed'**
String get decodeFail;
/// No description provided for @shareUrl.
///
/// In en, this message translates to:
/// **'Share Request URL'**
String get shareUrl;
/// No description provided for @shareCurl.
///
/// In en, this message translates to:
/// **'Share cURL Request'**
String get shareCurl;
/// No description provided for @shareRequestResponse.
///
/// In en, this message translates to:
/// **'Share Request and Response'**
String get shareRequestResponse;
/// No description provided for @captureDetail.
///
/// In en, this message translates to:
/// **'Capture Detail'**
String get captureDetail;
/// No description provided for @proxyPinSoftware.
///
/// In en, this message translates to:
/// **'ProxyPin Open source traffic capture software for all platforms'**
String get proxyPinSoftware;
/// No description provided for @prompt.
///
/// In en, this message translates to:
/// **'Prompt'**
String get prompt;
/// No description provided for @curlSchemeRequest.
///
/// In en, this message translates to:
/// **'If the curl format is recognized, should it be converted into an HTTP request?'**
String get curlSchemeRequest;
/// No description provided for @appExitTips.
///
/// In en, this message translates to:
/// **'Press again to exit the program'**
String get appExitTips;
/// No description provided for @remoteConnectDisconnect.
///
/// In en, this message translates to:
/// **'Check remote connection failed, disconnected'**
String get remoteConnectDisconnect;
/// No description provided for @connect.
///
/// In en, this message translates to:
/// **'Connect'**
String get connect;
/// No description provided for @reconnect.
///
/// In en, this message translates to:
/// **'Reconnect'**
String get reconnect;
/// No description provided for @remoteConnected.
///
/// In en, this message translates to:
/// **'Connected {os}, traffic will be forwarded to {os}'**
String remoteConnected(Object os);
/// No description provided for @remoteConnectForward.
///
/// In en, this message translates to:
/// **'Remote connection, forwarding requests to other terminals'**
String get remoteConnectForward;
/// No description provided for @connectSuccess.
///
/// In en, this message translates to:
/// **'Connect successful'**
String get connectSuccess;
/// No description provided for @connectedRemote.
///
/// In en, this message translates to:
/// **'Connected to remote'**
String get connectedRemote;
/// No description provided for @connected.
///
/// In en, this message translates to:
/// **'Connected'**
String get connected;
/// No description provided for @notConnected.
///
/// In en, this message translates to:
/// **'Not connected'**
String get notConnected;
/// No description provided for @disconnect.
///
/// In en, this message translates to:
/// **'Disconnect'**
String get disconnect;
/// No description provided for @ipLayerProxy.
///
/// In en, this message translates to:
/// **'IP Layer Proxy(Beta)'**
String get ipLayerProxy;
/// No description provided for @ipLayerProxyDesc.
///
/// In en, this message translates to:
/// **'IP layer proxy can capture Flutter app requests, currently not very stable, welcome to submit PR'**
String get ipLayerProxyDesc;
/// No description provided for @inputAddress.
///
/// In en, this message translates to:
/// **'Input Address'**
String get inputAddress;
/// No description provided for @syncConfig.
///
/// In en, this message translates to:
/// **'Sync configuration'**
String get syncConfig;
/// No description provided for @pullConfigFail.
///
/// In en, this message translates to:
/// **'Failed to pull configuration, please check the network connection'**
String get pullConfigFail;
/// No description provided for @sync.
///
/// In en, this message translates to:
/// **'Sync'**
String get sync;
/// No description provided for @invalidQRCode.
///
/// In en, this message translates to:
/// **'Unrecognized QR code'**
String get invalidQRCode;
/// No description provided for @remoteConnectFail.
///
/// In en, this message translates to:
/// **'Connection failed,Please check if it is allowed on the same LAN and firewall, iOS needs to enable local network permissions'**
String get remoteConnectFail;
/// No description provided for @remoteConnectSuccessTips.
///
/// In en, this message translates to:
/// **'Your phone needs to enable packet capture in order to capture requests'**
String get remoteConnectSuccessTips;
/// No description provided for @windowMode.
///
/// In en, this message translates to:
/// **'Window Mode'**
String get windowMode;
/// No description provided for @windowModeSubTitle.
///
/// In en, this message translates to:
/// **'Enabled Packet Capture, Enter the background, Display a small window'**
String get windowModeSubTitle;
/// No description provided for @pipIcon.
///
/// In en, this message translates to:
/// **'Window shortcut icon'**
String get pipIcon;
/// No description provided for @pipIconDescribe.
///
/// In en, this message translates to:
/// **'Show quick access to small window Icon'**
String get pipIconDescribe;
/// No description provided for @headerExpanded.
///
/// In en, this message translates to:
/// **'Headers Expanded'**
String get headerExpanded;
/// No description provided for @headerExpandedSubtitle.
///
/// In en, this message translates to:
/// **'Details page Headers is expanded by default'**
String get headerExpandedSubtitle;
/// No description provided for @bottomNavigation.
///
/// In en, this message translates to:
/// **'Bottom Navigation'**
String get bottomNavigation;
/// No description provided for @bottomNavigationSubtitle.
///
/// In en, this message translates to:
/// **'Bottom navigation bar is displayed, effective after restart'**
String get bottomNavigationSubtitle;
/// No description provided for @memoryCleanup.
///
/// In en, this message translates to:
/// **'Memory Cleanup'**
String get memoryCleanup;
/// No description provided for @memoryCleanupSubtitle.
///
/// In en, this message translates to:
/// **'Automatically clean up requests on memory limit reached and keep 32 most recent after cleaning'**
String get memoryCleanupSubtitle;
/// No description provided for @unlimited.
///
/// In en, this message translates to:
/// **'Unlimited'**
String get unlimited;
/// No description provided for @custom.
///
/// In en, this message translates to:
/// **'Custom'**
String get custom;
/// No description provided for @externalProxyAuth.
///
/// In en, this message translates to:
/// **'Proxy Auth (Optional)'**
String get externalProxyAuth;
/// No description provided for @externalProxyServer.
///
/// In en, this message translates to:
/// **'Proxy Server'**
String get externalProxyServer;
/// No description provided for @externalProxyConnectFailure.
///
/// In en, this message translates to:
/// **'External Proxy Connect failure'**
String get externalProxyConnectFailure;
/// No description provided for @externalProxyFailureConfirm.
///
/// In en, this message translates to:
/// **'Access to all http will fail due to network connectivity issues,Do you want to continue setting up external proxies。'**
String get externalProxyFailureConfirm;
/// No description provided for @mobileDisplayPacketCapture.
///
/// In en, this message translates to:
/// **'Mobile Display Packet Capture:'**
String get mobileDisplayPacketCapture;
/// No description provided for @proxyPortRepeat.
///
/// In en, this message translates to:
/// **'Startup failed, please check the port number {port} is occupied。'**
String proxyPortRepeat(Object port);
/// No description provided for @reset.
///
/// In en, this message translates to:
/// **'Reset'**
String get reset;
/// No description provided for @proxyIgnoreDomain.
///
/// In en, this message translates to:
/// **'Proxy ignores domain'**
String get proxyIgnoreDomain;
/// No description provided for @domainWhitelistDescribe.
///
/// In en, this message translates to:
/// **'Only proxy domain names on the whitelist. If the whitelist is enabled, the blacklist will be invalid'**
String get domainWhitelistDescribe;
/// No description provided for @domainBlacklistDescribe.
///
/// In en, this message translates to:
/// **'Domain names on the blacklist will not be proxied'**
String get domainBlacklistDescribe;
/// No description provided for @domain.
///
/// In en, this message translates to:
/// **'Host'**
String get domain;
/// No description provided for @enableScript.
///
/// In en, this message translates to:
/// **'Enable Script'**
String get enableScript;
/// No description provided for @scriptUseDescribe.
///
/// In en, this message translates to:
/// **'Use JavaScript to modify requests and responses'**
String get scriptUseDescribe;
/// No description provided for @scriptEdit.
///
/// In en, this message translates to:
/// **'Edit script'**
String get scriptEdit;
/// No description provided for @scrollEnd.
///
/// In en, this message translates to:
/// **'Scroll to End'**
String get scrollEnd;
/// No description provided for @logger.
///
/// In en, this message translates to:
/// **'Log'**
String get logger;
/// No description provided for @material3.
///
/// In en, this message translates to:
/// **'Material 3 is the latest version of Google’s open-source design system'**
String get material3;
/// No description provided for @iosVpnBackgroundAudio.
///
/// In en, this message translates to:
/// **'After turning on packet capture, exit to the background. In order to maintain the main UI thread for network communication, a silent audio playback will be enabled to keep the main thread running. Otherwise, it will only run in the background for 30 seconds. Do you agree to play audio in the background after turning on packet capture?'**
String get iosVpnBackgroundAudio;
/// No description provided for @markRead.
///
/// In en, this message translates to:
/// **'Mark as read'**
String get markRead;
/// No description provided for @autoRead.
///
/// In en, this message translates to:
/// **'Auto read'**
String get autoRead;
/// No description provided for @highlight.
///
/// In en, this message translates to:
/// **'Highlight'**
String get highlight;
/// No description provided for @blue.
///
/// In en, this message translates to:
/// **'Blue'**
String get blue;
/// No description provided for @green.
///
/// In en, this message translates to:
/// **'Green'**
String get green;
/// No description provided for @yellow.
///
/// In en, this message translates to:
/// **'Yellow'**
String get yellow;
/// No description provided for @red.
///
/// In en, this message translates to:
/// **'Red'**
String get red;
/// No description provided for @pink.
///
/// In en, this message translates to:
/// **'Pink'**
String get pink;
/// No description provided for @gray.
///
/// In en, this message translates to:
/// **'Gray'**
String get gray;
/// No description provided for @underline.
///
/// In en, this message translates to:
/// **'Underline'**
String get underline;
/// No description provided for @requestBlock.
///
/// In en, this message translates to:
/// **'Request Block'**
String get requestBlock;
/// No description provided for @other.
///
/// In en, this message translates to:
/// **'Other'**
String get other;
/// No description provided for @certHashName.
///
/// In en, this message translates to:
/// **'CA Hash Name'**
String get certHashName;
/// No description provided for @regExp.
///
/// In en, this message translates to:
/// **'RegExp'**
String get regExp;
/// No description provided for @systemCertName.
///
/// In en, this message translates to:
/// **'System Certificate Name'**
String get systemCertName;
/// No description provided for @qrCode.
///
/// In en, this message translates to:
/// **'QR Code'**
String get qrCode;
/// No description provided for @scanQrCode.
///
/// In en, this message translates to:
/// **'Scan QR Code'**
String get scanQrCode;
/// No description provided for @generateQrCode.
///
/// In en, this message translates to:
/// **'Generate'**
String get generateQrCode;
/// No description provided for @saveImage.
///
/// In en, this message translates to:
/// **'Save Image'**
String get saveImage;
/// No description provided for @selectImage.
///
/// In en, this message translates to:
/// **'Select Image'**
String get selectImage;
/// No description provided for @inputContent.
///
/// In en, this message translates to:
/// **'Input Content'**
String get inputContent;
/// No description provided for @errorCorrectLevel.
///
/// In en, this message translates to:
/// **'Error Correct'**
String get errorCorrectLevel;
/// No description provided for @output.
///
/// In en, this message translates to:
/// **'Output'**
String get output;
/// No description provided for @timestamp.
///
/// In en, this message translates to:
/// **'Timestamp'**
String get timestamp;
/// No description provided for @convert.
///
/// In en, this message translates to:
/// **'Convert'**
String get convert;
/// No description provided for @time.
///
/// In en, this message translates to:
/// **'DateTime'**
String get time;
/// No description provided for @nowTimestamp.
///
/// In en, this message translates to:
/// **'Now timestamp'**
String get nowTimestamp;
/// No description provided for @hosts.
///
/// In en, this message translates to:
/// **'Hosts'**
String get hosts;
/// No description provided for @toAddress.
///
/// In en, this message translates to:
/// **'To Address'**
String get toAddress;
/// No description provided for @encrypt.
///
/// In en, this message translates to:
/// **'Encrypt'**
String get encrypt;
/// No description provided for @decrypt.
///
/// In en, this message translates to:
/// **'Decrypt'**
String get decrypt;
/// No description provided for @cipher.
///
/// In en, this message translates to:
/// **'Cipher'**
String get cipher;
/// No description provided for @appUpdateCheckVersion.
///
/// In en, this message translates to:
/// **'Check for Updates'**
String get appUpdateCheckVersion;
/// No description provided for @appUpdateNotAvailableMsg.
///
/// In en, this message translates to:
/// **'Already Using The Latest Version'**
String get appUpdateNotAvailableMsg;
/// No description provided for @appUpdateDialogTitle.
///
/// In en, this message translates to:
/// **'Update Available'**
String get appUpdateDialogTitle;
/// No description provided for @appUpdateUpdateMsg.
///
/// In en, this message translates to:
/// **'A new version of ProxyPin is available. Would you like to update now?'**
String get appUpdateUpdateMsg;
/// No description provided for @appUpdateCurrentVersionLbl.
///
/// In en, this message translates to:
/// **'Current Version'**
String get appUpdateCurrentVersionLbl;
/// No description provided for @appUpdateNewVersionLbl.
///
/// In en, this message translates to:
/// **'New Version'**
String get appUpdateNewVersionLbl;
/// No description provided for @appUpdateUpdateNowBtnTxt.
///
/// In en, this message translates to:
/// **'Update Now'**
String get appUpdateUpdateNowBtnTxt;
/// No description provided for @appUpdateLaterBtnTxt.
///
/// In en, this message translates to:
/// **'Later'**
String get appUpdateLaterBtnTxt;
/// No description provided for @appUpdateIgnoreBtnTxt.
///
/// In en, this message translates to:
/// **'Ignore'**
String get appUpdateIgnoreBtnTxt;
/// No description provided for @requestMap.
///
/// In en, this message translates to:
/// **'Request Map'**
String get requestMap;
/// No description provided for @requestMapDescribe.
///
/// In en, this message translates to:
/// **'Do not request remote services, use local configuration or script for response'**
String get requestMapDescribe;
/// No description provided for @automatic.
///
/// In en, this message translates to:
/// **'Automatic'**
String get automatic;
/// No description provided for @manual.
///
/// In en, this message translates to:
/// **'Manual'**
String get manual;
/// No description provided for @certNotInstalled.
///
/// In en, this message translates to:
/// **'Certificate not installed'**
String get certNotInstalled;
/// No description provided for @openNewWindow.
///
/// In en, this message translates to:
/// **'Open New Window'**
String get openNewWindow;
/// No description provided for @sponsorDonate.
///
/// In en, this message translates to:
/// **'Sponsor / Donate'**
String get sponsorDonate;
/// No description provided for @sponsorSupport.
///
/// In en, this message translates to:
/// **'Support ongoing development'**
String get sponsorSupport;
/// No description provided for @sponsorThanks.
///
/// In en, this message translates to:
/// **'Thank you for supporting this open-source project by choosing any of the following methods to help its long-term development.'**
String get sponsorThanks;
/// No description provided for @sponsorAfdian.
///
/// In en, this message translates to:
/// **'AFDIAN'**
String get sponsorAfdian;
/// No description provided for @sponsorBuyMeCoffee.
///
/// In en, this message translates to:
/// **'Buy Me a Coffee'**
String get sponsorBuyMeCoffee;
/// No description provided for @privacyPolicy.
///
/// In en, this message translates to:
/// **'Privacy Policy'**
String get privacyPolicy;
/// No description provided for @privacyContent.
///
/// In en, this message translates to:
/// **'This open-source packet capture tool runs entirely on your device. It has no backend server and does not collect, store, or upload any personal data. All captured traffic is processed locally and is only forwarded when you explicitly use remote forwarding. Permissions (e.g., network, storage, and camera for QR codes) are used solely to provide features. You can audit the behavior in the public source code.'**
String get privacyContent;
/// No description provided for @requestCrypto.
///
/// In en, this message translates to:
/// **'Request Crypto'**
String get requestCrypto;
/// No description provided for @cryptoDecoded.
///
/// In en, this message translates to:
/// **'Decoded'**
String get cryptoDecoded;
/// No description provided for @cryptoDecodeToggle.
///
/// In en, this message translates to:
/// **'Decrypt'**
String get cryptoDecodeToggle;
/// No description provided for @optional.
///
/// In en, this message translates to:
/// **'Optional'**
String get optional;
/// No description provided for @cryptoRuleField.
///
/// In en, this message translates to:
/// **'Field Name'**
String get cryptoRuleField;
/// No description provided for @cryptoIvPrefixLabel.
///
/// In en, this message translates to:
/// **'IV Prefix'**
String get cryptoIvPrefixLabel;
/// No description provided for @cryptoIvPrefixTooltip.
///
/// In en, this message translates to:
/// **'Use the first N bytes of the response body as IV'**
String get cryptoIvPrefixTooltip;
/// No description provided for @local.
///
/// In en, this message translates to:
/// **'Local'**
String get local;
/// No description provided for @remoteUrl.
///
/// In en, this message translates to:
/// **'Remote URL'**
String get remoteUrl;
/// No description provided for @view.
///
/// In en, this message translates to:
/// **'View'**
String get view;
}
class _AppLocalizationsDelegate extends LocalizationsDelegate {
const _AppLocalizationsDelegate();
@override
Future load(Locale locale) {
return SynchronousFuture(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when language+script codes are specified.
switch (locale.languageCode) {
case 'zh':
{
switch (locale.scriptCode) {
case 'Hant':
return AppLocalizationsZhHant();
}
break;
}
}
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'en':
return AppLocalizationsEn();
case 'zh':
return AppLocalizationsZh();
}
throw FlutterError('AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.');
}
================================================
FILE: lib/l10n/app_localizations_en.dart
================================================
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get breakpoint => 'Breakpoint';
@override
String get breakpointRule => 'Breakpoint Rule';
@override
String get name => 'Name';
@override
String get requests => 'Requests';
@override
String get favorites => 'Favorites';
@override
String get history => 'History';
@override
String get toolbox => 'Toolbox';
@override
String get preference => 'Preferences';
@override
String get feedback => 'Feedback';
@override
String get about => 'About';
@override
String get filter => 'Proxy Filter';
@override
String get script => 'Script';
@override
String get share => 'Share';
@override
String get port => 'Port: ';
@override
String get proxy => 'Proxy';
@override
String get externalProxy => 'External Proxy';
@override
String get username => 'Username';
@override
String get password => 'Password';
@override
String get proxySetting => 'Proxy Setting';
@override
String get setAs => 'Set as ';
@override
String get systemProxy => 'System Proxy';
@override
String get enabledHTTP2 => 'Enable HTTP2';
@override
String get serverNotStart => 'Proxy server not started';
@override
String get download => 'Download';
@override
String get config => 'Configuration';
@override
String get version => 'Version';
@override
String get start => 'Start';
@override
String get stop => 'Stop';
@override
String get clear => 'Clear';
@override
String get httpsProxy => 'HTTPS Proxy';
@override
String get setting => 'Settings';
@override
String get mobileConnect => 'Mobile Connect';
@override
String get connectRemote => 'Connect Remote';
@override
String get remoteDevice => 'Remote Device';
@override
String get remoteDeviceList => 'Remote Device List';
@override
String get myQRCode => 'My QR Code';
@override
String get theme => 'Theme';
@override
String get followSystem => 'Follow System';
@override
String get themeColor => 'Theme Color';
@override
String get themeLight => 'Light';
@override
String get themeDark => 'Dark';
@override
String get language => 'Language';
@override
String get autoStartup => 'Auto Start Recording Traffic';
@override
String get autoStartupDescribe => 'Automatically start recording traffic when the program starts';
@override
String get copied => 'Copied to clipboard';
@override
String get execute => 'Execute';
@override
String get cancel => 'Cancel';
@override
String get close => 'Close';
@override
String get save => 'Save';
@override
String get confirm => 'Confirm';
@override
String get confirmTitle => 'Confirm operation';
@override
String get confirmContent => 'Are you sure about this operation?';
@override
String get addSuccess => 'Successfully added';
@override
String get saveSuccess => 'Saved successfully';
@override
String get operationSuccess => 'Operation succeeded';
@override
String get import => 'Import';
@override
String get importSuccess => 'Import successful';
@override
String get importFailed => 'Import failed';
@override
String get export => 'Export';
@override
String get exportSuccess => 'Export successful';
@override
String get exportFailed => 'Export failed';
@override
String get deleteSuccess => 'Delete successful';
@override
String get send => 'Send';
@override
String get fail => 'fail';
@override
String get success => 'success';
@override
String get emptyData => 'Empty Data';
@override
String get requestSuccess => 'Request successful';
@override
String get add => 'Add';
@override
String get all => 'All';
@override
String get modify => 'Modify';
@override
String get responseType => 'Response Type';
@override
String get request => 'Request';
@override
String get response => 'Response';
@override
String get statusCode => 'Status code';
@override
String get duration => 'Duration';
@override
String get done => 'Done';
@override
String get type => 'Type';
@override
String get enable => 'Enable';
@override
String get example => 'Example: ';
@override
String get responseHeader => 'Headers';
@override
String get requestHeader => 'Headers';
@override
String get requestLine => 'Request Line';
@override
String get requestMethod => 'Request Method';
@override
String get param => 'Param';
@override
String get replaceBodyWith => 'Replace Body With:';
@override
String get redirectTo => 'Redirect To:';
@override
String get redirect => 'Redirect';
@override
String get cannotBeEmpty => 'Cannot be empty';
@override
String get requestRewriteList => 'Request Rewrite List';
@override
String get requestRewriteRule => 'Request Rewrite Rule';
@override
String get requestRewriteEnable => 'Enable Request Rewrite';
@override
String get action => 'Action';
@override
String get multiple => 'Multiple';
@override
String get edit => 'Edit';
@override
String get disabled => 'Disabled';
@override
String requestRewriteDeleteConfirm(Object size) {
return 'Delete $size rule(s)?';
}
@override
String get useGuide => 'Use Guide';
@override
String get pleaseEnter => 'Please Enter';
@override
String get click => 'Click';
@override
String get replace => 'Replace';
@override
String get clickEdit => 'Click Edit';
@override
String get refresh => 'Refresh';
@override
String get selectFile => 'Select file';
@override
String get match => 'Match';
@override
String get value => 'Value';
@override
String get matchRule => 'Match Rule';
@override
String get emptyMatchAll => 'Empty means match all';
@override
String get newBuilt => 'New';
@override
String get reportServers => 'Report Servers';
@override
String get addReportServer => 'Add Report Server';
@override
String get editReportServer => 'Edit Report Server';
@override
String get serverUrl => 'Server URL';
@override
String get compression => 'Compression';
@override
String get compressionNone => 'None';
@override
String get newFolder => 'New Folder';
@override
String get enableSelect => 'Enable Select';
@override
String get disableSelect => 'Disable Select';
@override
String get deleteSelect => 'Delete Select';
@override
String get testData => 'Test Data';
@override
String get noChangesDetected => 'No changes detected';
@override
String get enterMatchData => 'Enter the data to be matched';
@override
String get modifyRequestHeader => 'Modify Header';
@override
String get headerName => 'Header Name';
@override
String get headerValue => 'Header Value';
@override
String get deleteHeaderConfirm => 'Do you want to delete the request header?';
@override
String get sequence => 'All Requests';
@override
String get domainList => 'Domain List';
@override
String get domainWhitelist => 'Proxy Domain Whitelist';
@override
String get domainBlacklist => 'Proxy Domain Blacklist';
@override
String get domainFilter => 'Proxy Domain List';
@override
String get appWhitelist => 'App Whitelist';
@override
String get appWhitelistDescribe =>
'Only proxy Apps on the whitelist. If the whitelist is enabled, the blacklist will be invalid';
@override
String get appBlacklist => 'App Blacklist';
@override
String get scanCode => 'Scan Code Connect';
@override
String get addBlacklist => 'Add Proxy Blacklist';
@override
String get addWhitelist => 'Add Proxy Whitelist';
@override
String get deleteWhitelist => 'Delete Proxy Whitelist';
@override
String domainListSubtitle(Object count, Object time) {
return 'Last Request Time: $time, Count: $count';
}
@override
String get selectAction => 'Select action';
@override
String get copy => 'Copy';
@override
String get copyHost => 'Copy Host';
@override
String get copyUrl => 'Copy URL';
@override
String get copyRawRequest => 'Copy Raw Request';
@override
String get copyRequestResponse => 'Copy Request and Response';
@override
String get copyCurl => 'Copy cURL';
@override
String get copyAsPythonRequests => 'Copy as Python Requests';
@override
String get delete => 'Delete';
@override
String get rename => 'Rename';
@override
String get repeat => 'Repeat';
@override
String get repeatAllRequests => 'Repeat All Requests';
@override
String get repeatDomainRequests => 'Repeat Domain Requests';
@override
String get customRepeat => 'Custom Repeat';
@override
String get repeatCount => 'Iterations';
@override
String get repeatInterval => 'Interval(ms)';
@override
String get repeatDelay => 'Delay(ms)';
@override
String get scheduleTime => 'Schedule Time';
@override
String get fixed => 'fixed';
@override
String get random => 'random';
@override
String get keepCustomSettings => 'Keep custom settings';
@override
String get editRequest => 'Edit and Request';
@override
String get reSendRequest => 'The request has been resent';
@override
String get viewExport => 'View Export';
@override
String get timeDesc => 'Descending by time';
@override
String get timeAsc => 'Ascending by time';
@override
String get search => 'Search';
@override
String get clearSearch => 'Clear Search';
@override
String get requestType => 'Request type';
@override
String get keyword => 'Keyword';
@override
String get keywordSearchScope => 'Keyword search scope: ';
@override
String get favorite => 'Favorite';
@override
String get deleteFavorite => 'Delete Favorite';
@override
String get emptyFavorite => 'Empty Favorite';
@override
String get deleteFavoriteSuccess => 'Favorite deleted';
@override
String get historyRecord => 'History';
@override
String get historyCacheTime => 'Cache Time';
@override
String get historyManualSave => 'Manual Save';
@override
String historyDay(Object day) {
return '$day days';
}
@override
String get historyForever => 'Forever';
@override
String historyRecordTitle(Object length, Object name) {
return '$name Records $length';
}
@override
String get historyEmptyName => 'Name cannot be empty';
@override
String historySubtitle(Object requestLength, Object size) {
return 'Records $requestLength file $size';
}
@override
String get historyUnSave => 'Current record is not saved';
@override
String get historyDeleteConfirm => 'Do you want to delete this history?';
@override
String get requestEdit => 'Request Editing';
@override
String get encode => 'Encode';
@override
String get requestBody => 'Request Body';
@override
String get responseBody => 'Response Body';
@override
String get requestRewrite => 'Request Rewrite';
@override
String get newWindow => 'New Window';
@override
String get httpRequest => 'HTTP Request';
@override
String get enabledHttps => 'Enable HTTPS Proxy';
@override
String get installRootCa => 'Install Certificate';
@override
String get installCaLocal => 'Install Certificate to Local-Machine';
@override
String get downloadRootCa => 'Download Certificate';
@override
String get downloadRootCaNote =>
'Note: If you set the default browser to other than Safari, click this line to copy and paste the link to Safari browser';
@override
String get generateCA => 'Generate new root certificate';
@override
String get generateCADescribe =>
'Are you sure you want to generate a new root certificate? If confirmed,\nYou need to reinstall and trust the new certificate';
@override
String get resetDefaultCA => 'Reset Default Root Certificate';
@override
String get resetDefaultCADescribe =>
'Are you sure you want to reset to the default root certificate?\nProxyPin default root certificate is the same for all users.';
@override
String get exportCaP12 => 'Export Root Certificate(.p12)';
@override
String get importCaP12 => 'Import Root Certificate(.p12)';
@override
String get trustCa => 'Trust Certificate';
@override
String get profileDownload => 'Profile Download';
@override
String get exportCA => 'Export Root Certificate';
@override
String get exportPrivateKey => 'Export Private Key';
@override
String get install => 'Install';
@override
String get installCaDescribe => 'Install CA Setting > Profile Download > Install';
@override
String get trustCaDescribe => 'Trust CA Setting > General > About > Certificate Trust Setting';
@override
String get androidRoot => 'System Certificate (ROOT Device)';
@override
String get androidRootMagisk =>
'Magisk module: \nAndroid ROOT devices can be used Magisk ProxyPinCA System Certificate Module, After installing and restarting the phone Check the system certificate to see if there is a ProxyPinCA certificate. If there is, it indicates that the certificate has been successfully installed。';
@override
String androidRootRename(Object name) {
return 'If the module does not take effect, you can install the system root certificate according to the online tutorial, and name the root certificate $name';
}
@override
String get androidRootCADownload => 'Download System Certificate(.0)';
@override
String get androidUserCA => 'User Certificate';
@override
String get androidUserCATips => 'Tips: Android7+ many apps will not trust user certificates';
@override
String get androidUserCAInstall =>
'Open settings -> Security -> Encryption and credentials -> Install certificate -> CA certificate';
@override
String get androidUserXposed =>
'It is recommended to use the Xposed module for packet capture (no need for ROOT), click to view wiki';
@override
String get configWifiProxy => 'Configure mobile Wi-Fi proxy';
@override
String get caInstallGuide => 'Certificate Installation Guide';
@override
String get caAndroidBrowser => 'Open Google Browser on Android devices:';
@override
String get caIosBrowser => 'Open Safari on iOS devices:';
@override
String get localIP => 'Local IP ';
@override
String get mobileScan => 'Configure Wi-Fi proxy or Scan with Mobile App';
@override
String get decode => 'Decode';
@override
String get encodeInput => 'Enter the content to be converted';
@override
String get encodeResult => 'Conversion Result';
@override
String get encodeFail => 'Encoding failed';
@override
String get decodeFail => 'Decoding failed';
@override
String get shareUrl => 'Share Request URL';
@override
String get shareCurl => 'Share cURL Request';
@override
String get shareRequestResponse => 'Share Request and Response';
@override
String get captureDetail => 'Capture Detail';
@override
String get proxyPinSoftware => 'ProxyPin Open source traffic capture software for all platforms';
@override
String get prompt => 'Prompt';
@override
String get curlSchemeRequest => 'If the curl format is recognized, should it be converted into an HTTP request?';
@override
String get appExitTips => 'Press again to exit the program';
@override
String get remoteConnectDisconnect => 'Check remote connection failed, disconnected';
@override
String get connect => 'Connect';
@override
String get reconnect => 'Reconnect';
@override
String remoteConnected(Object os) {
return 'Connected $os, traffic will be forwarded to $os';
}
@override
String get remoteConnectForward => 'Remote connection, forwarding requests to other terminals';
@override
String get connectSuccess => 'Connect successful';
@override
String get connectedRemote => 'Connected to remote';
@override
String get connected => 'Connected';
@override
String get notConnected => 'Not connected';
@override
String get disconnect => 'Disconnect';
@override
String get ipLayerProxy => 'IP Layer Proxy(Beta)';
@override
String get ipLayerProxyDesc =>
'IP layer proxy can capture Flutter app requests, currently not very stable, welcome to submit PR';
@override
String get inputAddress => 'Input Address';
@override
String get syncConfig => 'Sync configuration';
@override
String get pullConfigFail => 'Failed to pull configuration, please check the network connection';
@override
String get sync => 'Sync';
@override
String get invalidQRCode => 'Unrecognized QR code';
@override
String get remoteConnectFail =>
'Connection failed,Please check if it is allowed on the same LAN and firewall, iOS needs to enable local network permissions';
@override
String get remoteConnectSuccessTips => 'Your phone needs to enable packet capture in order to capture requests';
@override
String get windowMode => 'Window Mode';
@override
String get windowModeSubTitle => 'Enabled Packet Capture, Enter the background, Display a small window';
@override
String get pipIcon => 'Window shortcut icon';
@override
String get pipIconDescribe => 'Show quick access to small window Icon';
@override
String get headerExpanded => 'Headers Expanded';
@override
String get headerExpandedSubtitle => 'Details page Headers is expanded by default';
@override
String get bottomNavigation => 'Bottom Navigation';
@override
String get bottomNavigationSubtitle => 'Bottom navigation bar is displayed, effective after restart';
@override
String get memoryCleanup => 'Memory Cleanup';
@override
String get memoryCleanupSubtitle =>
'Automatically clean up requests on memory limit reached and keep 32 most recent after cleaning';
@override
String get unlimited => 'Unlimited';
@override
String get custom => 'Custom';
@override
String get externalProxyAuth => 'Proxy Auth (Optional)';
@override
String get externalProxyServer => 'Proxy Server';
@override
String get externalProxyConnectFailure => 'External Proxy Connect failure';
@override
String get externalProxyFailureConfirm =>
'Access to all http will fail due to network connectivity issues,Do you want to continue setting up external proxies。';
@override
String get mobileDisplayPacketCapture => 'Mobile Display Packet Capture:';
@override
String proxyPortRepeat(Object port) {
return 'Startup failed, please check the port number $port is occupied。';
}
@override
String get reset => 'Reset';
@override
String get proxyIgnoreDomain => 'Proxy ignores domain';
@override
String get domainWhitelistDescribe =>
'Only proxy domain names on the whitelist. If the whitelist is enabled, the blacklist will be invalid';
@override
String get domainBlacklistDescribe => 'Domain names on the blacklist will not be proxied';
@override
String get domain => 'Host';
@override
String get enableScript => 'Enable Script';
@override
String get scriptUseDescribe => 'Use JavaScript to modify requests and responses';
@override
String get scriptEdit => 'Edit script';
@override
String get scrollEnd => 'Scroll to End';
@override
String get logger => 'Log';
@override
String get material3 => 'Material 3 is the latest version of Google’s open-source design system';
@override
String get iosVpnBackgroundAudio =>
'After turning on packet capture, exit to the background. In order to maintain the main UI thread for network communication, a silent audio playback will be enabled to keep the main thread running. Otherwise, it will only run in the background for 30 seconds. Do you agree to play audio in the background after turning on packet capture?';
@override
String get markRead => 'Mark as read';
@override
String get autoRead => 'Auto read';
@override
String get highlight => 'Highlight';
@override
String get blue => 'Blue';
@override
String get green => 'Green';
@override
String get yellow => 'Yellow';
@override
String get red => 'Red';
@override
String get pink => 'Pink';
@override
String get gray => 'Gray';
@override
String get underline => 'Underline';
@override
String get requestBlock => 'Request Block';
@override
String get other => 'Other';
@override
String get certHashName => 'CA Hash Name';
@override
String get regExp => 'RegExp';
@override
String get systemCertName => 'System Certificate Name';
@override
String get qrCode => 'QR Code';
@override
String get scanQrCode => 'Scan QR Code';
@override
String get generateQrCode => 'Generate';
@override
String get saveImage => 'Save Image';
@override
String get selectImage => 'Select Image';
@override
String get inputContent => 'Input Content';
@override
String get errorCorrectLevel => 'Error Correct';
@override
String get output => 'Output';
@override
String get timestamp => 'Timestamp';
@override
String get convert => 'Convert';
@override
String get time => 'DateTime';
@override
String get nowTimestamp => 'Now timestamp';
@override
String get hosts => 'Hosts';
@override
String get toAddress => 'To Address';
@override
String get encrypt => 'Encrypt';
@override
String get decrypt => 'Decrypt';
@override
String get cipher => 'Cipher';
@override
String get appUpdateCheckVersion => 'Check for Updates';
@override
String get appUpdateNotAvailableMsg => 'Already Using The Latest Version';
@override
String get appUpdateDialogTitle => 'Update Available';
@override
String get appUpdateUpdateMsg => 'A new version of ProxyPin is available. Would you like to update now?';
@override
String get appUpdateCurrentVersionLbl => 'Current Version';
@override
String get appUpdateNewVersionLbl => 'New Version';
@override
String get appUpdateUpdateNowBtnTxt => 'Update Now';
@override
String get appUpdateLaterBtnTxt => 'Later';
@override
String get appUpdateIgnoreBtnTxt => 'Ignore';
@override
String get requestMap => 'Request Map';
@override
String get requestMapDescribe => 'Do not request remote services, use local configuration or script for response';
@override
String get automatic => 'Automatic';
@override
String get manual => 'Manual';
@override
String get certNotInstalled => 'Certificate not installed';
@override
String get openNewWindow => 'Open New Window';
@override
String get sponsorDonate => 'Sponsor / Donate';
@override
String get sponsorSupport => 'Support ongoing development';
@override
String get sponsorThanks =>
'Thank you for supporting this open-source project by choosing any of the following methods to help its long-term development.';
@override
String get sponsorAfdian => 'AFDIAN';
@override
String get sponsorBuyMeCoffee => 'Buy Me a Coffee';
@override
String get privacyPolicy => 'Privacy Policy';
@override
String get privacyContent =>
'This open-source packet capture tool runs entirely on your device. It has no backend server and does not collect, store, or upload any personal data. All captured traffic is processed locally and is only forwarded when you explicitly use remote forwarding. Permissions (e.g., network, storage, and camera for QR codes) are used solely to provide features. You can audit the behavior in the public source code.';
@override
String get requestCrypto => 'Request Crypto';
@override
String get cryptoDecoded => 'Decoded';
@override
String get cryptoDecodeToggle => 'Decrypt';
@override
String get optional => 'Optional';
@override
String get cryptoRuleField => 'Field Name';
@override
String get cryptoIvPrefixLabel => 'IV Prefix';
@override
String get cryptoIvPrefixTooltip => 'Use the first N bytes of the response body as IV';
@override
String get local => 'Local';
@override
String get remoteUrl => 'Remote URL';
@override
String get view => 'View';
}
================================================
FILE: lib/l10n/app_localizations_zh.dart
================================================
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Chinese (`zh`).
class AppLocalizationsZh extends AppLocalizations {
AppLocalizationsZh([String locale = 'zh']) : super(locale);
@override
String get breakpoint => '断点';
@override
String get breakpointRule => '断点规则';
@override
String get name => '名称';
@override
String get requests => '抓包';
@override
String get favorites => '收藏';
@override
String get history => '历史';
@override
String get toolbox => '工具箱';
@override
String get preference => '偏好设置';
@override
String get feedback => '反馈';
@override
String get about => '关于';
@override
String get filter => '代理过滤';
@override
String get script => '脚本';
@override
String get share => '分享';
@override
String get port => '端口号: ';
@override
String get proxy => '代理';
@override
String get externalProxy => '外部代理设置';
@override
String get username => '用户名';
@override
String get password => '密码';
@override
String get proxySetting => '代理设置';
@override
String get setAs => '设置为';
@override
String get systemProxy => '系统代理';
@override
String get enabledHTTP2 => '启用HTTP2';
@override
String get serverNotStart => '未开启抓包';
@override
String get download => '下载';
@override
String get config => '配置';
@override
String get version => '版本';
@override
String get start => '开始';
@override
String get stop => '停止';
@override
String get clear => '清空';
@override
String get httpsProxy => 'HTTPS 代理';
@override
String get setting => '设置';
@override
String get mobileConnect => '手机连接';
@override
String get connectRemote => '连接终端';
@override
String get remoteDevice => '远程设备';
@override
String get remoteDeviceList => '远程设备列表';
@override
String get myQRCode => '我的二维码';
@override
String get theme => '主题';
@override
String get followSystem => '跟随系统';
@override
String get themeColor => '主题颜色';
@override
String get themeLight => '浅色';
@override
String get themeDark => '深色';
@override
String get language => '语言';
@override
String get autoStartup => '自动开启抓包';
@override
String get autoStartupDescribe => '程序启动时自动开始记录流量';
@override
String get copied => '已复制到剪切板';
@override
String get execute => '执行';
@override
String get cancel => '取消';
@override
String get close => '关闭';
@override
String get save => '保存';
@override
String get confirm => '确认';
@override
String get confirmTitle => '确认操作';
@override
String get confirmContent => '是否确认此操作?';
@override
String get addSuccess => '添加成功';
@override
String get saveSuccess => '保存成功';
@override
String get operationSuccess => '操作成功';
@override
String get import => '导入';
@override
String get importSuccess => '导入成功';
@override
String get importFailed => '导入失败';
@override
String get export => '导出';
@override
String get exportSuccess => '导出成功';
@override
String get exportFailed => '导出失败';
@override
String get deleteSuccess => '删除成功';
@override
String get send => '发送';
@override
String get fail => '失败';
@override
String get success => '成功';
@override
String get emptyData => '无数据';
@override
String get requestSuccess => '请求成功';
@override
String get add => '添加';
@override
String get all => '全部';
@override
String get modify => '修改';
@override
String get responseType => '响应类型';
@override
String get request => '请求';
@override
String get response => '响应';
@override
String get statusCode => '状态码';
@override
String get duration => '耗时';
@override
String get done => '完成';
@override
String get type => '类型';
@override
String get enable => '启用';
@override
String get example => '示例: ';
@override
String get responseHeader => '响应头';
@override
String get requestHeader => '请求头';
@override
String get requestLine => '请求行';
@override
String get requestMethod => '请求方法';
@override
String get param => '参数';
@override
String get replaceBodyWith => '消息体替换为:';
@override
String get redirectTo => '重定向到:';
@override
String get redirect => '重定向';
@override
String get cannotBeEmpty => '不能为空';
@override
String get requestRewriteList => '请求重写列表';
@override
String get requestRewriteRule => '请求重写规则';
@override
String get requestRewriteEnable => '是否启用请求重写';
@override
String get action => '行为';
@override
String get multiple => '多选';
@override
String get edit => '编辑';
@override
String get disabled => '禁用';
@override
String requestRewriteDeleteConfirm(Object size) {
return '是否删除$size条规则?';
}
@override
String get useGuide => '使用文档';
@override
String get pleaseEnter => '请输入';
@override
String get click => '点击';
@override
String get replace => '替换';
@override
String get clickEdit => '点击编辑';
@override
String get refresh => '刷新';
@override
String get selectFile => '选择文件';
@override
String get match => '匹配';
@override
String get value => '值';
@override
String get matchRule => '匹配规则';
@override
String get emptyMatchAll => '为空表示匹配全部';
@override
String get newBuilt => '新建';
@override
String get reportServers => '上报服务器';
@override
String get addReportServer => '新增上报服务器';
@override
String get editReportServer => '编辑上报服务器';
@override
String get serverUrl => '服务器 URL';
@override
String get compression => '压缩';
@override
String get compressionNone => '无';
@override
String get newFolder => '新建文件夹';
@override
String get enableSelect => '启用选择';
@override
String get disableSelect => '禁用选择';
@override
String get deleteSelect => '删除选择';
@override
String get testData => '测试数据';
@override
String get noChangesDetected => '未检测到变更';
@override
String get enterMatchData => '输入待匹配的数据';
@override
String get modifyRequestHeader => '修改请求头';
@override
String get headerName => '请求头名称';
@override
String get headerValue => '请求头值';
@override
String get deleteHeaderConfirm => '是否删除该请求头';
@override
String get sequence => '全部请求';
@override
String get domainList => '域名列表';
@override
String get domainWhitelist => '代理域名白名单';
@override
String get domainBlacklist => '代理域名黑名单';
@override
String get domainFilter => '域名代理列表';
@override
String get appWhitelist => '应用白名单';
@override
String get appWhitelistDescribe => '只代理白名单中的应用, 白名单启用黑名单将会失效';
@override
String get appBlacklist => '应用黑名单';
@override
String get scanCode => '扫码连接';
@override
String get addBlacklist => '添加代理黑名单';
@override
String get addWhitelist => '添加代理白名单';
@override
String get deleteWhitelist => '删除代理白名单';
@override
String domainListSubtitle(Object count, Object time) {
return '最后请求时间: $time, 次数: $count';
}
@override
String get selectAction => '选择操作';
@override
String get copy => '复制';
@override
String get copyHost => '复制域名';
@override
String get copyUrl => '复制URL';
@override
String get copyRawRequest => '复制 原始请求';
@override
String get copyRequestResponse => '复制 请求和响应';
@override
String get copyCurl => '复制 cURL';
@override
String get copyAsPythonRequests => '复制 Python Requests';
@override
String get delete => '删除';
@override
String get rename => '重命名';
@override
String get repeat => '重放';
@override
String get repeatAllRequests => '重放所有请求';
@override
String get repeatDomainRequests => '重放域名下请求';
@override
String get customRepeat => '高级重放';
@override
String get repeatCount => '次数';
@override
String get repeatInterval => '间隔(ms)';
@override
String get repeatDelay => '延时(ms)';
@override
String get scheduleTime => '指定时间';
@override
String get fixed => '固定';
@override
String get random => '随机';
@override
String get keepCustomSettings => '保持自定义设置';
@override
String get editRequest => '编辑请求';
@override
String get reSendRequest => '已重新发送请求';
@override
String get viewExport => '视图导出';
@override
String get timeDesc => '按时间降序';
@override
String get timeAsc => '按时间升序';
@override
String get search => '搜索';
@override
String get clearSearch => '清除搜索';
@override
String get requestType => '请求类型';
@override
String get keyword => '关键词';
@override
String get keywordSearchScope => '关键词搜索范围: ';
@override
String get favorite => '收藏';
@override
String get deleteFavorite => '删除收藏';
@override
String get emptyFavorite => '暂无收藏';
@override
String get deleteFavoriteSuccess => '已删除收藏';
@override
String get historyRecord => '历史记录';
@override
String get historyCacheTime => '缓存时间';
@override
String get historyManualSave => '手动保存';
@override
String historyDay(Object day) {
return '$day天';
}
@override
String get historyForever => '永久';
@override
String historyRecordTitle(Object length, Object name) {
return '$name 记录数 $length';
}
@override
String get historyEmptyName => '名称不能为空';
@override
String historySubtitle(Object requestLength, Object size) {
return '记录数 $requestLength 文件 $size';
}
@override
String get historyUnSave => '当前会话记录未保存';
@override
String get historyDeleteConfirm => '是否删除该历史记录?';
@override
String get requestEdit => '请求编辑';
@override
String get encode => '编码';
@override
String get requestBody => '请求体';
@override
String get responseBody => '响应体';
@override
String get requestRewrite => '请求重写';
@override
String get newWindow => '新窗口打开';
@override
String get httpRequest => 'HTTP请求';
@override
String get enabledHttps => '启用HTTPS代理';
@override
String get installRootCa => '安装根证书';
@override
String get installCaLocal => '安装根证书到本机';
@override
String get downloadRootCa => '下载根证书';
@override
String get downloadRootCaNote => '注意:如果您将默认浏览器设置为 Safari 以外的浏览器,请单击此行复制并粘贴 Safari 浏览器的链接';
@override
String get generateCA => '重新生成根证书';
@override
String get generateCADescribe => '您确定要生成新的根证书吗? 如果确认,\n则需要重新安装并信任新的证书';
@override
String get resetDefaultCA => '重置默认根证书';
@override
String get resetDefaultCADescribe => '确定要重置为默认根证书吗? ProxyPin默认\n根证书对所有用户都是相同的.';
@override
String get exportCaP12 => '导出根证书 (.p12)';
@override
String get importCaP12 => '导入根证书 (.p12)';
@override
String get trustCa => '信任证书';
@override
String get profileDownload => '已下载描述文件';
@override
String get exportCA => '导出根证书';
@override
String get exportPrivateKey => '导出私钥';
@override
String get install => '安装';
@override
String get installCaDescribe => '安装证书 设置 > 已下载描述文件 > 安装';
@override
String get trustCaDescribe => '信任证书 设置 > 通用 > 关于本机 > 证书信任设置';
@override
String get androidRoot => '系统证书 (ROOT设备)';
@override
String get androidRootMagisk =>
'Magisk模块: \n安卓ROOT设备可以使用Magisk ProxyPinCA系统证书模块, 安装完重启手机后 在系统证书查看是否有ProxyPinCA证书,如果有说明证书安装成功。';
@override
String androidRootRename(Object name) {
return '模块不生效可以根据网上教程安装系统根证书, 根证书命名成 $name';
}
@override
String get androidRootCADownload => '下载系统根证书(.0)';
@override
String get androidUserCA => '用户证书';
@override
String get androidUserCATips => '提示:Android7+ 很多软件不会信任用户证书';
@override
String get androidUserCAInstall => '打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书';
@override
String get androidUserXposed => '推荐使用Xposed模块抓包(无需ROOT), 点击查看wiki';
@override
String get configWifiProxy => '配置手机Wi-Fi代理';
@override
String get caInstallGuide => '证书安装指南';
@override
String get caAndroidBrowser => '在 Android 设备上打开浏览器访问:';
@override
String get caIosBrowser => '在 iOS 设备上打开 Safari访问:';
@override
String get localIP => '本地IP ';
@override
String get mobileScan => '配置Wi-Fi代理或使用手机版扫描二维码';
@override
String get decode => '解码';
@override
String get encodeInput => '输入要转换的内容';
@override
String get encodeResult => '转换结果';
@override
String get encodeFail => '编码失败';
@override
String get decodeFail => '解码失败';
@override
String get shareUrl => '分享请求链接';
@override
String get shareCurl => '分享 cURL 请求';
@override
String get shareRequestResponse => '分享请求和响应';
@override
String get captureDetail => '抓包详情';
@override
String get proxyPinSoftware => 'ProxyPin全平台开源抓包软件';
@override
String get prompt => '提示';
@override
String get curlSchemeRequest => '识别到curl格式,是否转换为HTTP请求?';
@override
String get appExitTips => '再按一次退出程序';
@override
String get remoteConnectDisconnect => '检查远程连接失败,已断开';
@override
String get connect => '连接';
@override
String get reconnect => '重新连接';
@override
String remoteConnected(Object os) {
return '已连接$os,流量将转发到$os';
}
@override
String get remoteConnectForward => '远程连接,将其他设备流量转发到当前设备';
@override
String get connectSuccess => '连接成功';
@override
String get connectedRemote => '已连接远程';
@override
String get connected => '已连接';
@override
String get notConnected => '未连接';
@override
String get disconnect => '断开连接';
@override
String get ipLayerProxy => 'IP层代理(Beta)';
@override
String get ipLayerProxyDesc => 'IP层代理可抓取Flutter应用请求,目前不是很稳定,欢迎提交PR';
@override
String get inputAddress => '输入地址';
@override
String get syncConfig => '同步配置';
@override
String get pullConfigFail => '拉取配置失败, 请检查网络连接';
@override
String get sync => '同步';
@override
String get invalidQRCode => '无法识别的二维码';
@override
String get remoteConnectFail => '连接失败,请检查是否在同一局域网和防火墙是否允许, ios需要开启本地网络权限';
@override
String get remoteConnectSuccessTips => '手机需要开启抓包才可以抓取请求哦';
@override
String get windowMode => '窗口模式';
@override
String get windowModeSubTitle => '开启抓包后 如果应用退回到后台,显示一个小窗口';
@override
String get pipIcon => '窗口快捷图标';
@override
String get pipIconDescribe => '展示快捷进入小窗口Icon';
@override
String get headerExpanded => 'Headers自动展开';
@override
String get headerExpandedSubtitle => '详情页Headers栏是否自动展开';
@override
String get bottomNavigation => '底部导航';
@override
String get bottomNavigationSubtitle => '底部导航栏是否显示,重启后生效';
@override
String get memoryCleanup => '内存清理';
@override
String get memoryCleanupSubtitle => '到内存限制自动清理请求,清理后保留最近32条请求';
@override
String get unlimited => '无限制';
@override
String get custom => '自定义';
@override
String get externalProxyAuth => '代理认证 (可选)';
@override
String get externalProxyServer => '代理服务器';
@override
String get externalProxyConnectFailure => '外部代理连接失败';
@override
String get externalProxyFailureConfirm => '网络不通所有接口将会访问失败,是否继续设置外部代理。';
@override
String get mobileDisplayPacketCapture => '手机端是否展示抓包:';
@override
String proxyPortRepeat(Object port) {
return '启动失败,请检查端口号$port是否被占用';
}
@override
String get reset => '重置';
@override
String get proxyIgnoreDomain => '代理忽略域名';
@override
String get domainWhitelistDescribe => '只代理白名单中的域名, 白名单启用黑名单将会失效';
@override
String get domainBlacklistDescribe => '黑名单中的域名不会代理';
@override
String get domain => '域名';
@override
String get enableScript => '启用脚本工具';
@override
String get scriptUseDescribe => '使用 JavaScript 修改请求和响应';
@override
String get scriptEdit => '编辑脚本';
@override
String get scrollEnd => '跟踪滚动';
@override
String get logger => '日志';
@override
String get material3 => 'Material3是谷歌开源设计系统的最新版本';
@override
String get iosVpnBackgroundAudio => '开启抓包后,退出到后台。为了维护主UI线程的网络通信,将启用静音音频播放以保持主线程运行。否则,它将只在后台运行30秒。您同意在启用抓包后在后台播放音频吗?';
@override
String get markRead => '标记已读';
@override
String get autoRead => '自动已读';
@override
String get highlight => '高亮';
@override
String get blue => '蓝色';
@override
String get green => '绿色';
@override
String get yellow => '黄色';
@override
String get red => '红色';
@override
String get pink => '粉色';
@override
String get gray => '灰色';
@override
String get underline => '下划线';
@override
String get requestBlock => '请求屏蔽';
@override
String get other => '其他';
@override
String get certHashName => '证书Hash名称';
@override
String get regExp => '正则表达式';
@override
String get systemCertName => '系统证书名称';
@override
String get qrCode => '二维码';
@override
String get scanQrCode => '扫描二维码';
@override
String get generateQrCode => '生成二维码';
@override
String get saveImage => '保存图片';
@override
String get selectImage => '选择图片';
@override
String get inputContent => '输入内容';
@override
String get errorCorrectLevel => '纠错等级';
@override
String get output => '输出';
@override
String get timestamp => '时间戳';
@override
String get convert => '转换';
@override
String get time => '时间';
@override
String get nowTimestamp => '当前时间戳(秒)';
@override
String get hosts => 'Hosts 映射';
@override
String get toAddress => '映射地址';
@override
String get encrypt => '加密';
@override
String get decrypt => '解密';
@override
String get cipher => '加解密';
@override
String get appUpdateCheckVersion => '检查更新';
@override
String get appUpdateNotAvailableMsg => '已是最新版本';
@override
String get appUpdateDialogTitle => '有可用更新';
@override
String get appUpdateUpdateMsg => 'ProxyPin 的新版本现已推出。您想现在更新吗?';
@override
String get appUpdateCurrentVersionLbl => '当前版本';
@override
String get appUpdateNewVersionLbl => '新版本';
@override
String get appUpdateUpdateNowBtnTxt => '现在更新';
@override
String get appUpdateLaterBtnTxt => '以后再说';
@override
String get appUpdateIgnoreBtnTxt => '忽略';
@override
String get requestMap => '请求映射';
@override
String get requestMapDescribe => '不请求远程服务,使用本地配置或脚本进行响应';
@override
String get automatic => '自动';
@override
String get manual => '手动';
@override
String get certNotInstalled => '证书未安装';
@override
String get openNewWindow => '新窗口打开';
@override
String get sponsorDonate => '赞助 / 捐赠';
@override
String get sponsorSupport => '支持项目持续开发';
@override
String get sponsorThanks => '感谢支持开源项目,可选择以下任意方式,帮助项目长期发展';
@override
String get sponsorAfdian => '爱发电赞助';
@override
String get sponsorBuyMeCoffee => 'Buy Me a Coffee';
@override
String get privacyPolicy => '隐私协议';
@override
String get privacyContent =>
'本项目为开源抓包工具,所有功能均在本地设备上运行;无任何后端服务器,不会收集、存储或上传任何用户信息。抓取的网络数据仅在本地处理,除非您主动使用远程转发功能。所需权限(如网络、存储、相机用于扫码)仅用于实现相应功能。您可在公开的源代码中审计其行为。';
@override
String get requestCrypto => '请求解密';
@override
String get cryptoDecoded => '已解密';
@override
String get cryptoDecodeToggle => '解密';
@override
String get optional => '可选';
@override
String get cryptoRuleField => '字段名称';
@override
String get cryptoIvPrefixLabel => 'IV 前缀';
@override
String get cryptoIvPrefixTooltip => '使用响应体前 N 个字节作为 IV';
@override
String get local => '本地';
@override
String get remoteUrl => '远程URL';
@override
String get view => '查看';
}
/// The translations for Chinese, using the Han script (`zh_Hant`).
class AppLocalizationsZhHant extends AppLocalizationsZh {
AppLocalizationsZhHant() : super('zh_Hant');
@override
String get breakpoint => '斷點';
@override
String get breakpointRule => '斷點規則';
@override
String get name => '名稱';
@override
String get requests => '抓包';
@override
String get favorites => '收藏';
@override
String get history => '歷史';
@override
String get toolbox => '工具箱';
@override
String get preference => '偏好設定';
@override
String get feedback => '意見回饋';
@override
String get about => '關於';
@override
String get filter => '代理過濾';
@override
String get script => '腳本';
@override
String get share => '分享';
@override
String get port => '連接埠號: ';
@override
String get proxy => '代理';
@override
String get externalProxy => '外部代理設定';
@override
String get username => '使用者名稱';
@override
String get password => '密碼';
@override
String get proxySetting => '代理設定';
@override
String get setAs => '設定為';
@override
String get systemProxy => '系統代理';
@override
String get enabledHTTP2 => '啟用HTTP2';
@override
String get serverNotStart => '未開啟抓包';
@override
String get download => '下載';
@override
String get config => '設定';
@override
String get version => '版本';
@override
String get start => '開始';
@override
String get stop => '停止';
@override
String get clear => '清空';
@override
String get httpsProxy => 'HTTPS 代理';
@override
String get setting => '設定';
@override
String get mobileConnect => '手機連接';
@override
String get connectRemote => '連接終端';
@override
String get remoteDevice => '遠端裝置';
@override
String get remoteDeviceList => '遠端裝置列表';
@override
String get myQRCode => '我的二維碼';
@override
String get theme => '主題';
@override
String get followSystem => '跟隨系統';
@override
String get themeColor => '主題顏色';
@override
String get themeLight => '淺色';
@override
String get themeDark => '深色';
@override
String get language => '語言';
@override
String get autoStartup => '自動開啟抓包';
@override
String get autoStartupDescribe => '程式啟動時自動開始記錄流量';
@override
String get copied => '已複製到剪切板';
@override
String get execute => '執行';
@override
String get cancel => '取消';
@override
String get close => '關閉';
@override
String get save => '儲存';
@override
String get confirm => '確認';
@override
String get confirmTitle => '確認操作';
@override
String get confirmContent => '是否確認此操作?';
@override
String get addSuccess => '新增成功';
@override
String get saveSuccess => '儲存成功';
@override
String get operationSuccess => '操作成功';
@override
String get import => '匯入';
@override
String get importSuccess => '匯入成功';
@override
String get importFailed => '匯入失敗';
@override
String get export => '匯出';
@override
String get exportSuccess => '匯出成功';
@override
String get deleteSuccess => '刪除成功';
@override
String get send => '傳送';
@override
String get fail => '失敗';
@override
String get success => '成功';
@override
String get emptyData => '無資料';
@override
String get requestSuccess => '請求成功';
@override
String get add => '新增';
@override
String get all => '全部';
@override
String get modify => '修改';
@override
String get responseType => '回應類型';
@override
String get request => '請求';
@override
String get response => '回應';
@override
String get statusCode => '狀態碼';
@override
String get duration => '耗時';
@override
String get done => '完成';
@override
String get type => '類型';
@override
String get enable => '啟用';
@override
String get example => '範例: ';
@override
String get responseHeader => '回應標頭';
@override
String get requestHeader => '請求標頭';
@override
String get requestLine => '請求行';
@override
String get requestMethod => '請求方法';
@override
String get param => '參數';
@override
String get replaceBodyWith => '訊息體替換為:';
@override
String get redirectTo => '重新導向到:';
@override
String get redirect => '重新導向';
@override
String get cannotBeEmpty => '不能為空';
@override
String get requestRewriteList => '請求重寫列表';
@override
String get requestRewriteRule => '請求重寫規則';
@override
String get requestRewriteEnable => '是否啟用請求重寫';
@override
String get action => '行為';
@override
String get multiple => '多選';
@override
String get edit => '編輯';
@override
String get disabled => '停用';
@override
String requestRewriteDeleteConfirm(Object size) {
return '是否刪除$size條規則?';
}
@override
String get useGuide => '使用文件';
@override
String get pleaseEnter => '請輸入';
@override
String get click => '點選';
@override
String get replace => '替換';
@override
String get clickEdit => '點選編輯';
@override
String get refresh => '重新整理';
@override
String get selectFile => '選擇檔案';
@override
String get match => '符合';
@override
String get value => '值';
@override
String get matchRule => '符合規則';
@override
String get emptyMatchAll => '為空表示符合全部';
@override
String get newBuilt => '新建';
@override
String get reportServers => '上報伺服器';
@override
String get addReportServer => '新增上報伺服器';
@override
String get editReportServer => '編輯上報伺服器';
@override
String get serverUrl => '伺服器 URL';
@override
String get compression => '壓縮';
@override
String get compressionNone => '無';
@override
String get newFolder => '新建資料夾';
@override
String get enableSelect => '啟用選擇';
@override
String get disableSelect => '停用選擇';
@override
String get deleteSelect => '刪除選擇';
@override
String get testData => '測試資料';
@override
String get noChangesDetected => '未檢測到變更';
@override
String get enterMatchData => '輸入待符合的資料';
@override
String get modifyRequestHeader => '修改請求標頭';
@override
String get headerName => '請求標頭名稱';
@override
String get headerValue => '請求標頭值';
@override
String get deleteHeaderConfirm => '是否刪除該請求標頭';
@override
String get sequence => '全部請求';
@override
String get domainList => '網域名稱列表';
@override
String get domainWhitelist => '代理網域名稱白名單';
@override
String get domainBlacklist => '代理網域名稱黑名單';
@override
String get domainFilter => '網域名稱代理列表';
@override
String get appWhitelist => '應用程式白名單';
@override
String get appWhitelistDescribe => '只代理白名單中的應用程式, 白名單啟用黑名單將會失效';
@override
String get appBlacklist => '應用程式黑名單';
@override
String get scanCode => '掃碼連接';
@override
String get addBlacklist => '新增代理黑名單';
@override
String get addWhitelist => '新增代理白名單';
@override
String get deleteWhitelist => '刪除代理白名單';
@override
String domainListSubtitle(Object count, Object time) {
return '最後請求時間: $time, 次數: $count';
}
@override
String get selectAction => '選擇操作';
@override
String get copy => '複製';
@override
String get copyHost => '複製網域名稱';
@override
String get copyUrl => '複製URL';
@override
String get copyRawRequest => '複製原始請求';
@override
String get copyRequestResponse => '複製 請求和回應';
@override
String get copyCurl => '複製 cURL';
@override
String get copyAsPythonRequests => '複製 Python Requests';
@override
String get delete => '刪除';
@override
String get rename => '重新命名';
@override
String get repeat => '重放';
@override
String get repeatAllRequests => '重放所有請求';
@override
String get repeatDomainRequests => '重放網域名稱下請求';
@override
String get customRepeat => '進階重放';
@override
String get repeatCount => '次數';
@override
String get repeatInterval => '間隔(ms)';
@override
String get repeatDelay => '延遲(ms)';
@override
String get scheduleTime => '指定時間';
@override
String get fixed => '固定';
@override
String get random => '隨機';
@override
String get keepCustomSettings => '保持自訂設定';
@override
String get editRequest => '編輯請求';
@override
String get reSendRequest => '已重新傳送請求';
@override
String get viewExport => '檢視匯出';
@override
String get timeDesc => '按時間降序';
@override
String get timeAsc => '按時間升序';
@override
String get search => '搜尋';
@override
String get clearSearch => '清除搜尋';
@override
String get requestType => '請求類型';
@override
String get keyword => '關鍵字';
@override
String get keywordSearchScope => '關鍵字搜尋範圍: ';
@override
String get favorite => '收藏';
@override
String get deleteFavorite => '刪除收藏';
@override
String get emptyFavorite => '暫無收藏';
@override
String get deleteFavoriteSuccess => '已刪除收藏';
@override
String get historyRecord => '歷史記錄';
@override
String get historyCacheTime => '快取時間';
@override
String get historyManualSave => '手動儲存';
@override
String historyDay(Object day) {
return '$day天';
}
@override
String get historyForever => '永久';
@override
String historyRecordTitle(Object length, Object name) {
return '$name 記錄數 $length';
}
@override
String get historyEmptyName => '名稱不能為空';
@override
String historySubtitle(Object requestLength, Object size) {
return '記錄數 $requestLength 檔案 $size';
}
@override
String get historyUnSave => '目前對話記錄未儲存';
@override
String get historyDeleteConfirm => '是否刪除該歷史記錄?';
@override
String get requestEdit => '請求編輯';
@override
String get encode => '編碼';
@override
String get requestBody => '請求體';
@override
String get responseBody => '回應體';
@override
String get requestRewrite => '請求重寫';
@override
String get newWindow => '新視窗開啟';
@override
String get httpRequest => 'HTTP請求';
@override
String get enabledHttps => '啟用HTTPS代理';
@override
String get installRootCa => '安裝根憑證';
@override
String get installCaLocal => '安裝根憑證到本機';
@override
String get downloadRootCa => '下載根憑證';
@override
String get downloadRootCaNote => '注意:如果您將預設瀏覽器設定為 Safari 以外的瀏覽器,請點選此行複製並貼上 Safari 瀏覽器的連結';
@override
String get generateCA => '重新產生根憑證';
@override
String get generateCADescribe => '您確定要產生新的根憑證嗎? 如果確認,\n則需要重新安裝並信任新的憑證';
@override
String get resetDefaultCA => '重置預設根憑證';
@override
String get resetDefaultCADescribe => '確定要重置為預設根憑證嗎? ProxyPin預設\n根憑證對所有使用者都是相同的.';
@override
String get exportCaP12 => '匯出根憑證 (.p12)';
@override
String get importCaP12 => '匯入根憑證 (.p12)';
@override
String get trustCa => '信任憑證';
@override
String get profileDownload => '已下載描述檔案';
@override
String get exportCA => '匯出根憑證';
@override
String get exportPrivateKey => '匯出私鑰';
@override
String get install => '安裝';
@override
String get installCaDescribe => '安裝憑證 設定 > 已下載描述檔案 > 安裝';
@override
String get trustCaDescribe => '信任憑證 設定 > 一般 > 關於本機 > 憑證信任設定';
@override
String get androidRoot => '系統憑證 (ROOT裝置)';
@override
String get androidRootMagisk =>
'Magisk模組: \n安卓ROOT裝置可以使用Magisk ProxyPinCA系統憑證模組, 安裝完重新開機後 在系統憑證檢視是否有ProxyPinCA憑證,如果有說明憑證安裝成功。';
@override
String androidRootRename(Object name) {
return '模組不生效可以根據網上教學安裝系統根憑證, 根憑證命名成 $name';
}
@override
String get androidRootCADownload => '下載系統根憑證(.0)';
@override
String get androidUserCA => '使用者憑證';
@override
String get androidUserCATips => '提示:Android7+ 很多軟體不會信任使用者憑證';
@override
String get androidUserCAInstall => '開啟設定 -> 安全性 -> 加密和憑證 -> 安裝憑證 -> CA 憑證';
@override
String get androidUserXposed => '推薦使用Xposed模組抓包(無需ROOT), 點選檢視wiki';
@override
String get configWifiProxy => '設定手機Wi-Fi代理';
@override
String get caInstallGuide => '憑證安裝指南';
@override
String get caAndroidBrowser => '在 Android 裝置上開啟瀏覽器存取:';
@override
String get caIosBrowser => '在 iOS 裝置上開啟 Safari存取:';
@override
String get localIP => '本機IP ';
@override
String get mobileScan => '設定Wi-Fi代理或使用手機版掃描二維碼';
@override
String get decode => '解碼';
@override
String get encodeInput => '輸入要轉換的內容';
@override
String get encodeResult => '轉換結果';
@override
String get encodeFail => '編碼失敗';
@override
String get decodeFail => '解碼失敗';
@override
String get shareUrl => '分享請求連結';
@override
String get shareCurl => '分享 cURL 請求';
@override
String get shareRequestResponse => '分享請求和回應';
@override
String get captureDetail => '抓包詳情';
@override
String get proxyPinSoftware => 'ProxyPin全平台開源抓包軟體';
@override
String get prompt => '提示';
@override
String get curlSchemeRequest => '識別到curl格式,是否轉換為HTTP請求?';
@override
String get appExitTips => '再按一次退出程式';
@override
String get remoteConnectDisconnect => '檢查遠端連接失敗,已中斷連接';
@override
String get connect => '連接';
@override
String get reconnect => '重新連接';
@override
String remoteConnected(Object os) {
return '已連接$os,流量將轉發到$os';
}
@override
String get remoteConnectForward => '遠端連接,將其他裝置流量轉發到目前裝置';
@override
String get connectSuccess => '連接成功';
@override
String get connectedRemote => '已連接遠端';
@override
String get connected => '已連接';
@override
String get notConnected => '未連接';
@override
String get disconnect => '中斷連接';
@override
String get ipLayerProxy => 'IP層代理(Beta)';
@override
String get ipLayerProxyDesc => 'IP層代理可抓取Flutter應用程式請求,目前不是很穩定,歡迎提交PR';
@override
String get inputAddress => '輸入地址';
@override
String get syncConfig => '同步設定';
@override
String get pullConfigFail => '拉取設定失敗, 請檢查網路連接';
@override
String get sync => '同步';
@override
String get invalidQRCode => '無法識別的二維碼';
@override
String get remoteConnectFail => '連接失敗,請檢查是否在同一區域網路和防火牆是否允許, ios需要開啟本機網路權限';
@override
String get remoteConnectSuccessTips => '手機需要開啟抓包才可以抓取請求哦';
@override
String get windowMode => '視窗模式';
@override
String get windowModeSubTitle => '開啟抓包後 如果應用程式退回到背景,顯示一個小視窗';
@override
String get pipIcon => '視窗快捷圖示';
@override
String get pipIconDescribe => '展示快捷進入小視窗Icon';
@override
String get headerExpanded => 'Headers自動展開';
@override
String get headerExpandedSubtitle => '詳情頁Headers欄是否自動展開';
@override
String get bottomNavigation => '底部導航';
@override
String get bottomNavigationSubtitle => '底部導航欄是否顯示,重新啟動後生效';
@override
String get memoryCleanup => '記憶體清理';
@override
String get memoryCleanupSubtitle => '到記憶體限制自動清理請求,清理後保留最近32條請求';
@override
String get unlimited => '無限制';
@override
String get custom => '自訂';
@override
String get externalProxyAuth => '代理認證 (可選)';
@override
String get externalProxyServer => '代理伺服器';
@override
String get externalProxyConnectFailure => '外部代理連接失敗';
@override
String get externalProxyFailureConfirm => '網路不通所有介面將會存取失敗,是否繼續設定外部代理。';
@override
String get mobileDisplayPacketCapture => '手機端是否展示抓包:';
@override
String proxyPortRepeat(Object port) {
return '啟動失敗,請檢查連接埠號$port是否被占用';
}
@override
String get reset => '重置';
@override
String get proxyIgnoreDomain => '代理忽略網域名稱';
@override
String get domainWhitelistDescribe => '只代理白名單中的網域名稱, 白名單啟用黑名單將會失效';
@override
String get domainBlacklistDescribe => '黑名單中的網域名稱不會代理';
@override
String get domain => '網域名稱';
@override
String get enableScript => '啟用腳本工具';
@override
String get scriptUseDescribe => '使用 JavaScript 修改請求和回應';
@override
String get scriptEdit => '編輯腳本';
@override
String get scrollEnd => '跟蹤滾動';
@override
String get logger => '日誌';
@override
String get material3 => 'Material3是Google開源設計系統的最新版本';
@override
String get iosVpnBackgroundAudio =>
'開啟抓包後,退出到背景。為了維護主UI執行緒的網路通信,將啟用靜音音訊播放以保持主執行緒運作。否則,它將只在背景運作30秒。您同意在啟用抓包後在背景播放音訊嗎?';
@override
String get markRead => '標記已讀';
@override
String get autoRead => '自動已讀';
@override
String get highlight => '高亮顯示';
@override
String get blue => '藍色';
@override
String get green => '綠色';
@override
String get yellow => '黃色';
@override
String get red => '紅色';
@override
String get pink => '粉色';
@override
String get gray => '灰色';
@override
String get underline => '底線';
@override
String get requestBlock => '請求阻擋';
@override
String get other => '其他';
@override
String get certHashName => '憑證Hash名稱';
@override
String get regExp => '正規表示式';
@override
String get systemCertName => '系統憑證名稱';
@override
String get qrCode => '二維碼';
@override
String get scanQrCode => '掃描二維碼';
@override
String get generateQrCode => '產生二維碼';
@override
String get saveImage => '儲存圖片';
@override
String get selectImage => '選擇圖片';
@override
String get inputContent => '輸入內容';
@override
String get errorCorrectLevel => '糾錯等級';
@override
String get output => '輸出';
@override
String get timestamp => '時間戳';
@override
String get convert => '轉換';
@override
String get time => '時間';
@override
String get nowTimestamp => '目前時間戳(秒)';
@override
String get hosts => 'Hosts 對應';
@override
String get toAddress => '對應地址';
@override
String get encrypt => '加密';
@override
String get decrypt => '解密';
@override
String get cipher => '密文';
@override
String get appUpdateCheckVersion => '檢查更新';
@override
String get appUpdateNotAvailableMsg => '已是最新版本';
@override
String get appUpdateDialogTitle => '有可用更新';
@override
String get appUpdateUpdateMsg => 'ProxyPin 的新版本現已推出。您想現在更新嗎?';
@override
String get appUpdateCurrentVersionLbl => '目前版本';
@override
String get appUpdateNewVersionLbl => '新版本';
@override
String get appUpdateUpdateNowBtnTxt => '現在更新';
@override
String get appUpdateLaterBtnTxt => '稍後再說';
@override
String get appUpdateIgnoreBtnTxt => '忽略';
@override
String get requestMap => '請求映射';
@override
String get requestMapDescribe => '不請求遠端服務,使用本地配置或腳本進行回應';
@override
String get automatic => '自動';
@override
String get manual => '手動';
@override
String get certNotInstalled => '未安裝憑證';
@override
String get openNewWindow => '新視窗開啟';
@override
String get sponsorDonate => '贊助 / 捐贈';
@override
String get sponsorSupport => '支持項目持續開發';
@override
String get sponsorThanks => '感謝支持開源項目,可選擇以下任意方式,幫助項目長期發展';
@override
String get sponsorAfdian => '愛發電贊助';
@override
String get sponsorBuyMeCoffee => 'Buy Me a Coffee';
@override
String get privacyPolicy => '隱私協議';
@override
String get privacyContent =>
'本專案為開源抓包工具,所有功能均在本機裝置上運行;無任何後端伺服器,不會蒐集、儲存或上傳任何使用者資訊。擷取的網路資料僅在本機處理,除非您主動使用遠端轉發功能。所需權限(如網路、儲存、相機用於掃碼)僅用於實現相應功能。您可在公開的原始碼中稽核其行為。';
@override
String get requestCrypto => '請求解密';
@override
String get cryptoDecoded => '已解密';
@override
String get cryptoDecodeToggle => '解密';
@override
String get optional => '可選';
@override
String get cryptoRuleField => '字段';
@override
String get cryptoIvPrefixLabel => 'IV 前綴';
@override
String get cryptoIvPrefixTooltip => '使用回應內容的前 N 個字節作為 IV';
@override
String get local => '本地';
@override
String get remoteUrl => '遠端URL';
@override
String get view => '檢視';
}
================================================
FILE: lib/l10n/app_zh.arb
================================================
{
"breakpoint": "断点",
"breakpointRule": "断点规则",
"name": "名称",
"requests": "抓包",
"favorites": "收藏",
"history": "历史",
"toolbox": "工具箱",
"preference": "偏好设置",
"feedback": "反馈",
"about": "关于",
"filter": "代理过滤",
"script": "脚本",
"share": "分享",
"port": "端口号: ",
"proxy": "代理",
"externalProxy": "外部代理设置",
"username": "用户名",
"password": "密码",
"proxySetting": "代理设置",
"setAs": "设置为",
"systemProxy": "系统代理",
"enabledHTTP2": "启用HTTP2",
"serverNotStart": "未开启抓包",
"download": "下载",
"config": "配置",
"version": "版本",
"start": "开始",
"stop": "停止",
"clear": "清空",
"httpsProxy": "HTTPS 代理",
"setting": "设置",
"mobileConnect": "手机连接",
"connectRemote": "连接终端",
"remoteDevice": "远程设备",
"remoteDeviceList": "远程设备列表",
"myQRCode": "我的二维码",
"theme": "主题",
"themeColor": "主题颜色",
"followSystem": "跟随系统",
"themeLight": "浅色",
"themeDark": "深色",
"language": "语言",
"autoStartup": "自动开启抓包",
"autoStartupDescribe": "程序启动时自动开始记录流量",
"copied": "已复制到剪切板",
"execute": "执行",
"cancel": "取消",
"close": "关闭",
"save": "保存",
"confirm": "确认",
"confirmTitle": "确认操作",
"confirmContent": "是否确认此操作?",
"addSuccess": "添加成功",
"saveSuccess": "保存成功",
"operationSuccess": "操作成功",
"import": "导入",
"importSuccess": "导入成功",
"importFailed": "导入失败",
"export": "导出",
"exportSuccess": "导出成功",
"exportFailed": "导出失败",
"deleteSuccess": "删除成功",
"send": "发送",
"fail": "失败",
"success": "成功",
"emptyData": "无数据",
"requestSuccess": "请求成功",
"add": "添加",
"all": "全部",
"modify": "修改",
"responseType": "响应类型",
"request": "请求",
"response": "响应",
"statusCode": "状态码",
"duration": "耗时",
"done": "完成",
"type": "类型",
"enable": "启用",
"example": "示例: ",
"responseHeader": "响应头",
"requestHeader": "请求头",
"requestLine": "请求行",
"requestMethod": "请求方法",
"param": "参数",
"replaceBodyWith": "消息体替换为:",
"redirectTo": "重定向到:",
"redirect": "重定向",
"cannotBeEmpty": "不能为空",
"requestRewriteList": "请求重写列表",
"requestRewriteRule": "请求重写规则",
"requestRewriteEnable": "是否启用请求重写",
"action": "行为",
"multiple": "多选",
"edit": "编辑",
"disabled": "禁用",
"requestRewriteDeleteConfirm": "是否删除{size}条规则?",
"useGuide": "使用文档",
"pleaseEnter": "请输入",
"click": "点击",
"replace": "替换",
"clickEdit": "点击编辑",
"refresh": "刷新",
"selectFile": "选择文件",
"match": "匹配",
"value": "值",
"matchRule": "匹配规则",
"emptyMatchAll": "为空表示匹配全部",
"newBuilt": "新建",
"reportServers": "上报服务器",
"addReportServer": "新增上报服务器",
"editReportServer": "编辑上报服务器",
"serverUrl": "服务器 URL",
"compression": "压缩",
"compressionNone": "无",
"newFolder": "新建文件夹",
"enableSelect": "启用选择",
"disableSelect": "禁用选择",
"deleteSelect": "删除选择",
"testData": "测试数据",
"noChangesDetected": "未检测到变更",
"enterMatchData": "输入待匹配的数据",
"modifyRequestHeader": "修改请求头",
"headerName": "请求头名称",
"headerValue": "请求头值",
"deleteHeaderConfirm": "是否删除该请求头",
"sequence": "全部请求",
"domainList": "域名列表",
"domainWhitelist": "代理域名白名单",
"domainBlacklist": "代理域名黑名单",
"appWhitelist": "应用白名单",
"appWhitelistDescribe": "只代理白名单中的应用, 白名单启用黑名单将会失效",
"appBlacklist": "应用黑名单",
"domainFilter": "域名代理列表",
"scanCode": "扫码连接",
"addBlacklist": "添加代理黑名单",
"addWhitelist": "添加代理白名单",
"deleteWhitelist": "删除代理白名单",
"domainListSubtitle": "最后请求时间: {time}, 次数: {count}",
"selectAction": "选择操作",
"copy": "复制",
"copyHost": "复制域名",
"copyUrl": "复制URL",
"copyRawRequest": "复制 原始请求",
"copyRequestResponse": "复制 请求和响应",
"copyCurl": "复制 cURL",
"copyAsPythonRequests": "复制 Python Requests",
"delete": "删除",
"rename": "重命名",
"repeat": "重放",
"repeatAllRequests": "重放所有请求",
"repeatDomainRequests": "重放域名下请求",
"customRepeat": "高级重放",
"repeatCount": "次数",
"repeatInterval": "间隔(ms)",
"repeatDelay": "延时(ms)",
"scheduleTime": "指定时间",
"fixed": "固定",
"random": "随机",
"keepCustomSettings": "保持自定义设置",
"editRequest": "编辑请求",
"reSendRequest": "已重新发送请求",
"viewExport": "视图导出",
"timeDesc": "按时间降序",
"timeAsc": "按时间升序",
"search": "搜索",
"clearSearch": "清除搜索",
"requestType": "请求类型",
"keyword": "关键词",
"keywordSearchScope": "关键词搜索范围: ",
"favorite": "收藏",
"deleteFavorite": "删除收藏",
"emptyFavorite": "暂无收藏",
"deleteFavoriteSuccess": "已删除收藏",
"historyRecord": "历史记录",
"historyManualSave": "手动保存",
"historyDay": "{day}天",
"historyForever": "永久",
"historyCacheTime": "缓存时间",
"historyEmptyName": "名称不能为空",
"historyRecordTitle": "{name} 记录数 {length}",
"historySubtitle": "记录数 {requestLength} 文件 {size}",
"historyUnSave": "当前会话记录未保存",
"historyDeleteConfirm": "是否删除该历史记录?",
"requestEdit": "请求编辑",
"encode": "编码",
"decode": "解码",
"requestBody": "请求体",
"responseBody": "响应体",
"requestRewrite": "请求重写",
"newWindow": "新窗口打开",
"httpRequest": "HTTP请求",
"enabledHttps": "启用HTTPS代理",
"installRootCa": "安装根证书",
"installCaLocal": "安装根证书到本机",
"downloadRootCa": "下载根证书",
"downloadRootCaNote": "注意:如果您将默认浏览器设置为 Safari 以外的浏览器,请单击此行复制并粘贴 Safari 浏览器的链接",
"generateCA": "重新生成根证书",
"generateCADescribe": "您确定要生成新的根证书吗? 如果确认,\n则需要重新安装并信任新的证书",
"resetDefaultCA": "重置默认根证书",
"resetDefaultCADescribe": "确定要重置为默认根证书吗? ProxyPin默认\n根证书对所有用户都是相同的.",
"exportCaP12": "导出根证书 (.p12)",
"importCaP12": "导入根证书 (.p12)",
"trustCa": "信任证书",
"exportCA": "导出根证书",
"exportPrivateKey": "导出私钥",
"profileDownload": "已下载描述文件",
"install": "安装",
"installCaDescribe": "安装证书 设置 > 已下载描述文件 > 安装",
"trustCaDescribe": "信任证书 设置 > 通用 > 关于本机 > 证书信任设置",
"androidRoot": "系统证书 (ROOT设备)",
"androidRootMagisk": "Magisk模块: \n安卓ROOT设备可以使用Magisk ProxyPinCA系统证书模块, 安装完重启手机后 在系统证书查看是否有ProxyPinCA证书,如果有说明证书安装成功。",
"androidRootRename": "模块不生效可以根据网上教程安装系统根证书, 根证书命名成 {name}",
"androidUserCA": "用户证书",
"androidUserCATips": "提示:Android7+ 很多软件不会信任用户证书",
"androidRootCADownload": "下载系统根证书(.0)",
"androidUserCAInstall": "打开设置 -> 安全 -> 加密和凭据 -> 安装证书 -> CA 证书",
"androidUserXposed": "推荐使用Xposed模块抓包(无需ROOT), 点击查看wiki",
"configWifiProxy": "配置手机Wi-Fi代理",
"caInstallGuide": "证书安装指南",
"caAndroidBrowser": "在 Android 设备上打开浏览器访问:",
"caIosBrowser": "在 iOS 设备上打开 Safari访问:",
"localIP": "本地IP ",
"mobileScan": "配置Wi-Fi代理或使用手机版扫描二维码",
"encodeInput": "输入要转换的内容",
"encodeResult": "转换结果",
"encodeFail": "编码失败",
"decodeFail": "解码失败",
"shareUrl": "分享请求链接",
"shareCurl": "分享 cURL 请求",
"shareRequestResponse": "分享请求和响应",
"captureDetail": "抓包详情",
"proxyPinSoftware": "ProxyPin全平台开源抓包软件",
"prompt": "提示",
"curlSchemeRequest": "识别到curl格式,是否转换为HTTP请求?",
"appExitTips": "再按一次退出程序",
"remoteConnectDisconnect": "检查远程连接失败,已断开",
"connect": "连接",
"reconnect": "重新连接",
"remoteConnected": "已连接{os},流量将转发到{os}",
"remoteConnectForward": "远程连接,将其他设备流量转发到当前设备",
"connectSuccess": "连接成功",
"connectedRemote": "已连接远程",
"connected": "已连接",
"notConnected": "未连接",
"inputAddress": "输入地址",
"disconnect": "断开连接",
"ipLayerProxy": "IP层代理(Beta)",
"ipLayerProxyDesc": "IP层代理可抓取Flutter应用请求,目前不是很稳定,欢迎提交PR",
"syncConfig": "同步配置",
"pullConfigFail": "拉取配置失败, 请检查网络连接",
"sync": "同步",
"invalidQRCode": "无法识别的二维码",
"remoteConnectFail": "连接失败,请检查是否在同一局域网和防火墙是否允许, ios需要开启本地网络权限",
"remoteConnectSuccessTips": "手机需要开启抓包才可以抓取请求哦",
"windowMode": "窗口模式",
"windowModeSubTitle": "开启抓包后 如果应用退回到后台,显示一个小窗口",
"pipIcon": "窗口快捷图标",
"pipIconDescribe": "展示快捷进入小窗口Icon",
"headerExpanded": "Headers自动展开",
"headerExpandedSubtitle": "详情页Headers栏是否自动展开",
"bottomNavigation": "底部导航",
"bottomNavigationSubtitle": "底部导航栏是否显示,重启后生效",
"memoryCleanup": "内存清理",
"memoryCleanupSubtitle": "到内存限制自动清理请求,清理后保留最近32条请求",
"unlimited": "无限制",
"custom": "自定义",
"externalProxyAuth": "代理认证 (可选)",
"externalProxyServer": "代理服务器",
"externalProxyConnectFailure": "外部代理连接失败",
"externalProxyFailureConfirm": "网络不通所有接口将会访问失败,是否继续设置外部代理。",
"mobileDisplayPacketCapture": "手机端是否展示抓包:",
"proxyPortRepeat": "启动失败,请检查端口号{port}是否被占用",
"reset": "重置",
"proxyIgnoreDomain": "代理忽略域名",
"domainWhitelistDescribe": "只代理白名单中的域名, 白名单启用黑名单将会失效",
"domainBlacklistDescribe": "黑名单中的域名不会代理",
"domain": "域名",
"enableScript": "启用脚本工具",
"scriptUseDescribe": "使用 JavaScript 修改请求和响应",
"scriptEdit": "编辑脚本",
"scrollEnd": "跟踪滚动",
"logger": "日志",
"material3": "Material3是谷歌开源设计系统的最新版本",
"iosVpnBackgroundAudio": "开启抓包后,退出到后台。为了维护主UI线程的网络通信,将启用静音音频播放以保持主线程运行。否则,它将只在后台运行30秒。您同意在启用抓包后在后台播放音频吗?",
"markRead": "标记已读",
"autoRead": "自动已读",
"highlight": "高亮",
"blue": "蓝色",
"green": "绿色",
"yellow": "黄色",
"red": "红色",
"pink": "粉色",
"gray": "灰色",
"underline": "下划线",
"requestBlock": "请求屏蔽",
"other": "其他",
"certHashName": "证书Hash名称",
"systemCertName": "系统证书名称",
"regExp": "正则表达式",
"qrCode": "二维码",
"generateQrCode": "生成二维码",
"scanQrCode": "扫描二维码",
"saveImage": "保存图片",
"selectImage": "选择图片",
"inputContent": "输入内容",
"errorCorrectLevel": "纠错等级",
"output": "输出",
"timestamp": "时间戳",
"convert": "转换",
"time": "时间",
"nowTimestamp": "当前时间戳(秒)",
"hosts": "Hosts 映射",
"toAddress": "映射地址",
"encrypt": "加密",
"decrypt": "解密",
"cipher": "加解密",
"appUpdateCheckVersion": "检查更新",
"appUpdateNotAvailableMsg": "已是最新版本",
"appUpdateDialogTitle": "有可用更新",
"appUpdateUpdateMsg": "ProxyPin 的新版本现已推出。您想现在更新吗?",
"appUpdateCurrentVersionLbl": "当前版本",
"appUpdateNewVersionLbl": "新版本",
"appUpdateUpdateNowBtnTxt": "现在更新",
"appUpdateLaterBtnTxt": "以后再说",
"appUpdateIgnoreBtnTxt": "忽略",
"requestMap": "请求映射",
"requestMapDescribe": "不请求远程服务,使用本地配置或脚本进行响应",
"automatic": "自动",
"manual": "手动",
"certNotInstalled": "证书未安装",
"openNewWindow": "新窗口打开",
"sponsorDonate": "赞助 / 捐赠",
"sponsorSupport": "支持项目持续开发",
"sponsorThanks": "感谢支持开源项目,可选择以下任意方式,帮助项目长期发展",
"sponsorAfdian": "爱发电赞助",
"sponsorBuyMeCoffee": "Buy Me a Coffee",
"privacyPolicy": "隐私协议",
"privacyContent": "本项目为开源抓包工具,所有功能均在本地设备上运行;无任何后端服务器,不会收集、存储或上传任何用户信息。抓取的网络数据仅在本地处理,除非您主动使用远程转发功能。所需权限(如网络、存储、相机用于扫码)仅用于实现相应功能。您可在公开的源代码中审计其行为。",
"requestCrypto": "请求解密",
"cryptoDecoded": "已解密",
"cryptoDecodeToggle": "解密",
"optional": "可选",
"cryptoRuleField": "字段名称",
"cryptoIvPrefixLabel": "IV 前缀",
"cryptoIvPrefixTooltip": "使用响应体前 N 个字节作为 IV",
"local": "本地",
"remoteUrl": "远程URL",
"view": "查看"
}
================================================
FILE: lib/l10n/app_zh_Hant.arb
================================================
{
"breakpoint": "斷點",
"breakpointRule": "斷點規則",
"requests": "抓包",
"favorites": "收藏",
"history": "歷史",
"toolbox": "工具箱",
"preference": "偏好設定",
"feedback": "意見回饋",
"about": "關於",
"filter": "代理過濾",
"script": "腳本",
"share": "分享",
"port": "連接埠號: ",
"proxy": "代理",
"externalProxy": "外部代理設定",
"username": "使用者名稱",
"password": "密碼",
"proxySetting": "代理設定",
"setAs": "設定為",
"systemProxy": "系統代理",
"enabledHTTP2": "啟用HTTP2",
"serverNotStart": "未開啟抓包",
"download": "下載",
"config": "設定",
"version": "版本",
"start": "開始",
"stop": "停止",
"clear": "清空",
"httpsProxy": "HTTPS 代理",
"setting": "設定",
"mobileConnect": "手機連接",
"connectRemote": "連接終端",
"remoteDevice": "遠端裝置",
"remoteDeviceList": "遠端裝置列表",
"myQRCode": "我的二維碼",
"theme": "主題",
"themeColor": "主題顏色",
"followSystem": "跟隨系統",
"themeLight": "淺色",
"themeDark": "深色",
"language": "語言",
"autoStartup": "自動開啟抓包",
"autoStartupDescribe": "程式啟動時自動開始記錄流量",
"copied": "已複製到剪切板",
"execute": "執行",
"cancel": "取消",
"close": "關閉",
"save": "儲存",
"confirm": "確認",
"confirmTitle": "確認操作",
"confirmContent": "是否確認此操作?",
"addSuccess": "新增成功",
"saveSuccess": "儲存成功",
"operationSuccess": "操作成功",
"import": "匯入",
"importSuccess": "匯入成功",
"importFailed": "匯入失敗",
"export": "匯出",
"exportSuccess": "匯出成功",
"deleteSuccess": "刪除成功",
"send": "傳送",
"fail": "失敗",
"success": "成功",
"emptyData": "無資料",
"requestSuccess": "請求成功",
"add": "新增",
"all": "全部",
"modify": "修改",
"responseType": "回應類型",
"request": "請求",
"response": "回應",
"statusCode": "狀態碼",
"duration": "耗時",
"done": "完成",
"type": "類型",
"enable": "啟用",
"example": "範例: ",
"responseHeader": "回應標頭",
"requestHeader": "請求標頭",
"requestLine": "請求行",
"requestMethod": "請求方法",
"param": "參數",
"replaceBodyWith": "訊息體替換為:",
"redirectTo": "重新導向到:",
"redirect": "重新導向",
"cannotBeEmpty": "不能為空",
"requestRewriteList": "請求重寫列表",
"requestRewriteRule": "請求重寫規則",
"requestRewriteEnable": "是否啟用請求重寫",
"action": "行為",
"multiple": "多選",
"edit": "編輯",
"disabled": "停用",
"requestRewriteDeleteConfirm": "是否刪除{size}條規則?",
"useGuide": "使用文件",
"pleaseEnter": "請輸入",
"click": "點選",
"replace": "替換",
"clickEdit": "點選編輯",
"refresh": "重新整理",
"selectFile": "選擇檔案",
"match": "符合",
"value": "值",
"matchRule": "符合規則",
"emptyMatchAll": "為空表示符合全部",
"newBuilt": "新建",
"reportServers": "上報伺服器",
"addReportServer": "新增上報伺服器",
"editReportServer": "編輯上報伺服器",
"serverUrl": "伺服器 URL",
"compression": "壓縮",
"compressionNone": "無",
"newFolder": "新建資料夾",
"enableSelect": "啟用選擇",
"disableSelect": "停用選擇",
"deleteSelect": "刪除選擇",
"testData": "測試資料",
"noChangesDetected": "未檢測到變更",
"enterMatchData": "輸入待符合的資料",
"modifyRequestHeader": "修改請求標頭",
"headerName": "請求標頭名稱",
"headerValue": "請求標頭值",
"deleteHeaderConfirm": "是否刪除該請求標頭",
"sequence": "全部請求",
"domainList": "網域名稱列表",
"domainWhitelist": "代理網域名稱白名單",
"domainBlacklist": "代理網域名稱黑名單",
"appWhitelist": "應用程式白名單",
"appWhitelistDescribe": "只代理白名單中的應用程式, 白名單啟用黑名單將會失效",
"appBlacklist": "應用程式黑名單",
"domainFilter": "網域名稱代理列表",
"scanCode": "掃碼連接",
"addBlacklist": "新增代理黑名單",
"addWhitelist": "新增代理白名單",
"deleteWhitelist": "刪除代理白名單",
"domainListSubtitle": "最後請求時間: {time}, 次數: {count}",
"selectAction": "選擇操作",
"copy": "複製",
"copyHost": "複製網域名稱",
"copyUrl": "複製URL",
"copyRawRequest": "複製原始請求",
"copyRequestResponse": "複製 請求和回應",
"copyCurl": "複製 cURL",
"copyAsPythonRequests": "複製 Python Requests",
"delete": "刪除",
"rename": "重新命名",
"repeat": "重放",
"repeatAllRequests": "重放所有請求",
"repeatDomainRequests": "重放網域名稱下請求",
"customRepeat": "進階重放",
"repeatCount": "次數",
"repeatInterval": "間隔(ms)",
"repeatDelay": "延遲(ms)",
"scheduleTime": "指定時間",
"fixed": "固定",
"random": "隨機",
"keepCustomSettings": "保持自訂設定",
"editRequest": "編輯請求",
"reSendRequest": "已重新傳送請求",
"viewExport": "檢視匯出",
"timeDesc": "按時間降序",
"timeAsc": "按時間升序",
"search": "搜尋",
"clearSearch": "清除搜尋",
"requestType": "請求類型",
"keyword": "關鍵字",
"keywordSearchScope": "關鍵字搜尋範圍: ",
"favorite": "收藏",
"deleteFavorite": "刪除收藏",
"emptyFavorite": "暫無收藏",
"deleteFavoriteSuccess": "已刪除收藏",
"name": "名稱",
"historyRecord": "歷史記錄",
"historyManualSave": "手動儲存",
"historyDay": "{day}天",
"historyForever": "永久",
"historyCacheTime": "快取時間",
"historyEmptyName": "名稱不能為空",
"historyRecordTitle": "{name} 記錄數 {length}",
"historySubtitle": "記錄數 {requestLength} 檔案 {size}",
"historyUnSave": "目前對話記錄未儲存",
"historyDeleteConfirm": "是否刪除該歷史記錄?",
"requestEdit": "請求編輯",
"encode": "編碼",
"decode": "解碼",
"requestBody": "請求體",
"responseBody": "回應體",
"requestRewrite": "請求重寫",
"newWindow": "新視窗開啟",
"httpRequest": "HTTP請求",
"enabledHttps": "啟用HTTPS代理",
"installRootCa": "安裝根憑證",
"installCaLocal": "安裝根憑證到本機",
"downloadRootCa": "下載根憑證",
"downloadRootCaNote": "注意:如果您將預設瀏覽器設定為 Safari 以外的瀏覽器,請點選此行複製並貼上 Safari 瀏覽器的連結",
"generateCA": "重新產生根憑證",
"generateCADescribe": "您確定要產生新的根憑證嗎? 如果確認,\n則需要重新安裝並信任新的憑證",
"resetDefaultCA": "重置預設根憑證",
"resetDefaultCADescribe": "確定要重置為預設根憑證嗎? ProxyPin預設\n根憑證對所有使用者都是相同的.",
"exportCaP12": "匯出根憑證 (.p12)",
"importCaP12": "匯入根憑證 (.p12)",
"trustCa": "信任憑證",
"exportCA": "匯出根憑證",
"exportPrivateKey": "匯出私鑰",
"profileDownload": "已下載描述檔案",
"install": "安裝",
"installCaDescribe": "安裝憑證 設定 > 已下載描述檔案 > 安裝",
"trustCaDescribe": "信任憑證 設定 > 一般 > 關於本機 > 憑證信任設定",
"androidRoot": "系統憑證 (ROOT裝置)",
"androidRootMagisk": "Magisk模組: \n安卓ROOT裝置可以使用Magisk ProxyPinCA系統憑證模組, 安裝完重新開機後 在系統憑證檢視是否有ProxyPinCA憑證,如果有說明憑證安裝成功。",
"androidRootRename": "模組不生效可以根據網上教學安裝系統根憑證, 根憑證命名成 {name}",
"androidUserCA": "使用者憑證",
"androidUserCATips": "提示:Android7+ 很多軟體不會信任使用者憑證",
"androidRootCADownload": "下載系統根憑證(.0)",
"androidUserCAInstall": "開啟設定 -> 安全性 -> 加密和憑證 -> 安裝憑證 -> CA 憑證",
"androidUserXposed": "推薦使用Xposed模組抓包(無需ROOT), 點選檢視wiki",
"configWifiProxy": "設定手機Wi-Fi代理",
"caInstallGuide": "憑證安裝指南",
"caAndroidBrowser": "在 Android 裝置上開啟瀏覽器存取:",
"caIosBrowser": "在 iOS 裝置上開啟 Safari存取:",
"localIP": "本機IP ",
"mobileScan": "設定Wi-Fi代理或使用手機版掃描二維碼",
"encodeInput": "輸入要轉換的內容",
"encodeResult": "轉換結果",
"encodeFail": "編碼失敗",
"decodeFail": "解碼失敗",
"shareUrl": "分享請求連結",
"shareCurl": "分享 cURL 請求",
"shareRequestResponse": "分享請求和回應",
"captureDetail": "抓包詳情",
"proxyPinSoftware": "ProxyPin全平台開源抓包軟體",
"prompt": "提示",
"curlSchemeRequest": "識別到curl格式,是否轉換為HTTP請求?",
"appExitTips": "再按一次退出程式",
"remoteConnectDisconnect": "檢查遠端連接失敗,已中斷連接",
"connect": "連接",
"reconnect": "重新連接",
"remoteConnected": "已連接{os},流量將轉發到{os}",
"remoteConnectForward": "遠端連接,將其他裝置流量轉發到目前裝置",
"connectSuccess": "連接成功",
"connectedRemote": "已連接遠端",
"connected": "已連接",
"notConnected": "未連接",
"inputAddress": "輸入地址",
"disconnect": "中斷連接",
"ipLayerProxy": "IP層代理(Beta)",
"ipLayerProxyDesc": "IP層代理可抓取Flutter應用程式請求,目前不是很穩定,歡迎提交PR",
"syncConfig": "同步設定",
"pullConfigFail": "拉取設定失敗, 請檢查網路連接",
"sync": "同步",
"invalidQRCode": "無法識別的二維碼",
"remoteConnectFail": "連接失敗,請檢查是否在同一區域網路和防火牆是否允許, ios需要開啟本機網路權限",
"remoteConnectSuccessTips": "手機需要開啟抓包才可以抓取請求哦",
"windowMode": "視窗模式",
"windowModeSubTitle": "開啟抓包後 如果應用程式退回到背景,顯示一個小視窗",
"pipIcon": "視窗快捷圖示",
"pipIconDescribe": "展示快捷進入小視窗Icon",
"headerExpanded": "Headers自動展開",
"headerExpandedSubtitle": "詳情頁Headers欄是否自動展開",
"bottomNavigation": "底部導航",
"bottomNavigationSubtitle": "底部導航欄是否顯示,重新啟動後生效",
"memoryCleanup": "記憶體清理",
"memoryCleanupSubtitle": "到記憶體限制自動清理請求,清理後保留最近32條請求",
"unlimited": "無限制",
"custom": "自訂",
"externalProxyAuth": "代理認證 (可選)",
"externalProxyServer": "代理伺服器",
"externalProxyConnectFailure": "外部代理連接失敗",
"externalProxyFailureConfirm": "網路不通所有介面將會存取失敗,是否繼續設定外部代理。",
"mobileDisplayPacketCapture": "手機端是否展示抓包:",
"proxyPortRepeat": "啟動失敗,請檢查連接埠號{port}是否被占用",
"reset": "重置",
"proxyIgnoreDomain": "代理忽略網域名稱",
"domainWhitelistDescribe": "只代理白名單中的網域名稱, 白名單啟用黑名單將會失效",
"domainBlacklistDescribe": "黑名單中的網域名稱不會代理",
"domain": "網域名稱",
"enableScript": "啟用腳本工具",
"scriptUseDescribe": "使用 JavaScript 修改請求和回應",
"scriptEdit": "編輯腳本",
"scrollEnd": "跟蹤滾動",
"logger": "日誌",
"material3": "Material3是Google開源設計系統的最新版本",
"iosVpnBackgroundAudio": "開啟抓包後,退出到背景。為了維護主UI執行緒的網路通信,將啟用靜音音訊播放以保持主執行緒運作。否則,它將只在背景運作30秒。您同意在啟用抓包後在背景播放音訊嗎?",
"markRead": "標記已讀",
"autoRead": "自動已讀",
"highlight": "高亮顯示",
"blue": "藍色",
"green": "綠色",
"yellow": "黃色",
"red": "紅色",
"pink": "粉色",
"gray": "灰色",
"underline": "底線",
"requestBlock": "請求阻擋",
"other": "其他",
"certHashName": "憑證Hash名稱",
"systemCertName": "系統憑證名稱",
"regExp": "正規表示式",
"qrCode": "二維碼",
"generateQrCode": "產生二維碼",
"scanQrCode": "掃描二維碼",
"saveImage": "儲存圖片",
"selectImage": "選擇圖片",
"inputContent": "輸入內容",
"errorCorrectLevel": "糾錯等級",
"output": "輸出",
"timestamp": "時間戳",
"convert": "轉換",
"time": "時間",
"nowTimestamp": "目前時間戳(秒)",
"hosts": "Hosts 對應",
"toAddress": "對應地址",
"encrypt": "加密",
"decrypt": "解密",
"cipher": "密文",
"requestCrypto": "請求解密",
"cryptoDecoded": "已解密",
"cryptoDecodeToggle": "解密",
"optional": "可選",
"cryptoRuleField": "字段",
"cryptoIvPrefixLabel": "IV 前綴",
"cryptoIvPrefixTooltip": "使用回應內容的前 N 個字節作為 IV",
"appUpdateCheckVersion": "檢查更新",
"appUpdateNotAvailableMsg": "已是最新版本",
"appUpdateDialogTitle": "有可用更新",
"appUpdateUpdateMsg": "ProxyPin 的新版本現已推出。您想現在更新嗎?",
"appUpdateCurrentVersionLbl": "目前版本",
"appUpdateNewVersionLbl": "新版本",
"appUpdateUpdateNowBtnTxt": "現在更新",
"appUpdateLaterBtnTxt": "稍後再說",
"appUpdateIgnoreBtnTxt": "忽略",
"requestMap": "請求映射",
"requestMapDescribe": "不請求遠端服務,使用本地配置或腳本進行回應",
"automatic": "自動",
"manual": "手動",
"certNotInstalled": "未安裝憑證",
"openNewWindow": "新視窗開啟",
"sponsorDonate": "贊助 / 捐贈",
"sponsorSupport": "支持項目持續開發",
"sponsorThanks": "感謝支持開源項目,可選擇以下任意方式,幫助項目長期發展",
"sponsorAfdian": "愛發電贊助",
"sponsorBuyMeCoffee": "Buy Me a Coffee",
"privacyPolicy": "隱私協議",
"privacyContent": "本專案為開源抓包工具,所有功能均在本機裝置上運行;無任何後端伺服器,不會蒐集、儲存或上傳任何使用者資訊。擷取的網路資料僅在本機處理,除非您主動使用遠端轉發功能。所需權限(如網路、儲存、相機用於掃碼)僅用於實現相應功能。您可在公開的原始碼中稽核其行為。",
"local": "本地",
"remoteUrl": "遠端URL",
"view": "檢視"
}
================================================
FILE: lib/main.dart
================================================
/*
* Copyright 2023 Hongen Wang All rights reserved.
*
* 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
*
* https://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.
*/
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:proxypin/network/bin/configuration.dart';
import 'package:proxypin/ui/component/chinese_font.dart';
import 'package:proxypin/ui/component/multi_window.dart';
import 'package:proxypin/ui/configuration.dart';
import 'package:proxypin/ui/desktop/desktop.dart';
import 'package:proxypin/ui/mobile/mobile.dart';
import 'package:proxypin/utils/desktop_support.dart';
import 'package:proxypin/utils/navigator.dart';
import 'package:proxypin/utils/platform.dart';
import 'l10n/app_localizations.dart';
///主入口
///@author wanghongen
void main(List args) async {
WidgetsFlutterBinding.ensureInitialized();
//多窗口
if (args.firstOrNull == 'multi_window') {
final windowId = int.parse(args[1]);
final argument = args[2].isEmpty ? const {} : jsonDecode(args[2]) as Map;
runApp(FluentApp(multiWindow(windowId, argument), (await AppConfiguration.instance)));
return;
}
var instance = AppConfiguration.instance;
var configuration = Configuration.instance;
//移动端
if (Platforms.isMobile()) {
var appConfiguration = await instance;
runApp(FluentApp(MobileHomePage((await configuration), appConfiguration), appConfiguration));
return;
}
var appConfiguration = await instance;
if (Platforms.isDesktop()) {
await DesktopSupport.initialize(appConfiguration);
}
runApp(FluentApp(DesktopHomePage(await configuration, appConfiguration), appConfiguration));
}
class FluentApp extends StatelessWidget {
final Widget home;
final AppConfiguration appConfiguration;
const FluentApp(this.home, this.appConfiguration, {super.key});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: appConfiguration.globalChange,
builder: (_, current, __) {
return MaterialApp(
title: 'ProxyPin',
debugShowCheckedModeBanner: false,
navigatorKey: navigatorHelper.navigatorKey,
theme: theme(Brightness.light),
darkTheme: theme(Brightness.dark),
themeMode: appConfiguration.themeMode,
locale: appConfiguration.language,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: home,
);
});
}
ThemeData theme(Brightness brightness) {
bool useMaterial3 = appConfiguration.useMaterial3;
bool isDark = brightness == Brightness.dark;
Color? themeColor = isDark ? appConfiguration.themeColor : appConfiguration.themeColor;
Color? cardColor = isDark ? Color(0XFF3C3C3C) : Colors.white;
Color? surfaceContainer = isDark ? Colors.grey[800] : Colors.white;
Color? secondary = useMaterial3 ? null : themeColor;
if (themeColor is MaterialColor) {
secondary = themeColor[500];
}
var colorScheme = ColorScheme.fromSeed(
brightness: brightness,
seedColor: themeColor,
primary: themeColor,
surface: cardColor,
secondary: secondary,
onPrimary: isDark ? Colors.white : null,
surfaceContainer: surfaceContainer,
surfaceContainerHigh: surfaceContainer,
);
var themeData =
ThemeData(brightness: brightness, useMaterial3: appConfiguration.useMaterial3, colorScheme: colorScheme);
if (!appConfiguration.useMaterial3) {
themeData = themeData.copyWith(
appBarTheme: themeData.appBarTheme.copyWith(
iconTheme: themeData.iconTheme.copyWith(size: 20),
backgroundColor: themeData.canvasColor,
elevation: 0,
titleTextStyle: themeData.textTheme.titleMedium,
),
tabBarTheme: themeData.tabBarTheme.copyWith(
labelColor: themeData.colorScheme.primary,
indicatorColor: themeColor,
unselectedLabelColor: themeData.textTheme.titleMedium?.color,
),
);
}
if (Platform.isWindows) {
themeData = themeData.useSystemChineseFont();
}
return themeData.copyWith(
dialogTheme:
themeData.dialogTheme.copyWith(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))));
}
}
================================================
FILE: lib/native/app_lifecycle.dart
================================================
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:proxypin/network/util/logger.dart';
abstract interface class LifecycleListener {
void onUserLeaveHint() {}
void onPictureInPictureModeChanged(bool isInPictureInPictureMode) {}
}
class AppLifecycleBinding {
static const MethodChannel _methodChannel = MethodChannel('com.proxy/appLifecycle');
//单例对象
static AppLifecycleBinding get instance {
_instance ??= AppLifecycleBinding._();
return _instance!;
}
final List _listeners = [];
static AppLifecycleBinding? _instance;
AppLifecycleBinding._() {
//注册方法
_methodChannel.setMethodCallHandler(_methodCallHandler);
}
static AppLifecycleBinding ensureInitialized() {
return AppLifecycleBinding.instance;
}
void addListener(LifecycleListener listener) {
if (_listeners.contains(listener)) return;
_listeners.add(listener);
}
void removeListener(LifecycleListener listener) {
_listeners.remove(listener);
}
Future _methodCallHandler(MethodCall call) async {
logger.d("AppLifecycle methodCallHandler ${call.method}");
switch (call.method) {
case 'appDetached':
await WidgetsBinding.instance.handleRequestAppExit();
break;
case 'onUserLeaveHint':
for (var listener in _listeners) {
listener.onUserLeaveHint();
}
break;
case 'onPictureInPictureModeChanged':
for (var listener in _listeners) {
listener.onPictureInPictureModeChanged(call.arguments);
}
break;
}
return Future.value();
}
}
================================================
FILE: lib/native/installed_apps.dart
================================================
import 'package:flutter/services.dart';
class InstalledApps {
static const MethodChannel _methodChannel = MethodChannel('com.proxy/installedApps');
static Future> getInstalledApps(
bool withIcon, {
String? packageNamePrefix,
bool includeSystemApps = false,
}) {
return _methodChannel.invokeListMethod