refactor ws relay

This commit is contained in:
luvletter2333 2022-06-22 00:54:05 +08:00
parent e44072e04b
commit 58d91d99aa
No known key found for this signature in database
GPG Key ID: A26A8880836E1978
12 changed files with 355 additions and 876 deletions

View File

@ -59,7 +59,7 @@ import tw.nekomimi.nekogram.proxy.ProxyManager;
import tw.nekomimi.nekogram.proxy.ShadowsocksLoader;
import tw.nekomimi.nekogram.proxy.ShadowsocksRLoader;
import tw.nekomimi.nekogram.proxy.VmessLoader;
import tw.nekomimi.nekogram.proxy.WsLoader;
import tw.nekomimi.nekogram.proxy.tcp2ws.WsLoader;
import tw.nekomimi.nekogram.proxy.SubInfo;
import tw.nekomimi.nekogram.proxy.SubManager;
import tw.nekomimi.nekogram.utils.AlertUtil;

View File

@ -912,29 +912,19 @@ public class LaunchActivity extends BasePermissionsActivity implements ActionBar
MediaController.getInstance().setBaseActivity(this, true);
UIUtil.runOnIoDispatcher(() -> {
ExternalGcm.checkUpdate(this);
if (NekoConfig.autoUpdateSubInfo.Bool()) for (SubInfo subInfo : SubManager.getSubList().find()) {
if (subInfo == null || !subInfo.enable) continue;
try {
subInfo.proxies = subInfo.reloadProxies();
subInfo.lastFetch = System.currentTimeMillis();
SubManager.getSubList().update(subInfo, true);
SharedConfig.reloadProxyList();
} catch (IOException allTriesFailed) {
FileLog.e(allTriesFailed);
// ExternalGcm.checkUpdate(this);
if (NekoConfig.autoUpdateSubInfo.Bool())
for (SubInfo subInfo : SubManager.getSubList().find()) {
if (subInfo == null || !subInfo.enable) continue;
try {
subInfo.proxies = subInfo.reloadProxies();
subInfo.lastFetch = System.currentTimeMillis();
SubManager.getSubList().update(subInfo, true);
SharedConfig.reloadProxyList();
} catch (IOException allTriesFailed) {
FileLog.e(allTriesFailed);
}
}
}
});
//FileLog.d("UI create time = " + (SystemClock.elapsedRealtime() - ApplicationLoader.startTime));

View File

@ -20,20 +20,16 @@ fun loadProxiesPublic(urls: List<String>, exceptions: MutableMap<String, Excepti
return emptyList()
// Try DoH first ( github.com is often blocked
try {
var content = DnsFactory.getTxts("nachonekodayo.sekai.icu").joinToString()
val content = DnsFactory.getTxts("nachonekodayo.sekai.icu").joinToString()
val proxiesString = StrUtil.getSubString(content, "#NekoXStart#", "#NekoXEnd#")
if (proxiesString.equals(content)) {
throw Exception("DoH get public proxy: Not found")
}
val proxies = parseProxies(proxiesString)
if (proxies.count() == 0) {
throw Exception("DoH get public proxy: Empty")
}
return proxies
return parseProxies(proxiesString)
} catch (e: Exception) {
FileLog.e(e.stackTraceToString())
FileLog.e(e)
}
// Try Other Urls

View File

@ -43,6 +43,7 @@ import org.telegram.ui.Components.LayoutHelper;
import java.util.ArrayList;
import cn.hutool.core.util.StrUtil;
import tw.nekomimi.nekogram.proxy.tcp2ws.WsLoader;
public class WsSettingsActivity extends BaseFragment {

View File

@ -1,300 +0,0 @@
package tw.nekomimi.nekogram.proxy.tcp2ws;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.telegram.messenger.BuildConfig;
import org.telegram.messenger.FileLog;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Objects;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okhttp3.internal.NativeImageTestsAccessorsKt;
import okio.ByteString;
import tw.nekomimi.nekogram.proxy.WsLoader;
public class ProxyHandler implements Runnable {
private InputStream m_ClientInput = null;
private OutputStream m_ClientOutput = null;
private Object m_lock;
private Socks4Impl comm = null;
Socket m_ClientSocket;
WebSocket m_ServerSocket = null;
Socket m_ServerSocketRaw = null;
Throwable error = null;
HashMap<String, Integer> mapper;
WsLoader.Bean bean;
byte[] m_Buffer = new byte[SocksConstants.DEFAULT_BUF_SIZE];
public ProxyHandler(Socket clientSocket, HashMap<String, Integer> mapper, WsLoader.Bean bean) {
this.mapper = mapper;
this.bean = bean;
this.m_ClientSocket = clientSocket;
try {
m_ClientSocket.setSoTimeout(SocksConstants.DEFAULT_PROXY_TIMEOUT);
} catch (SocketException e) {
FileLog.e("Socket Exception during seting Timeout.");
}
FileLog.d("Proxy Created.");
}
public void setLock(Object lock) {
m_lock = lock;
}
public void run() {
FileLog.d("Proxy Started.");
setLock(this);
if (prepareClient()) {
processRelay();
close();
} else {
FileLog.e("Proxy - client socket is null !");
}
}
public void close() {
try {
if (m_ClientOutput != null) {
m_ClientOutput.flush();
m_ClientOutput.close();
}
} catch (IOException e) {
// ignore
}
try {
if (m_ClientSocket != null) {
m_ClientSocket.close();
}
} catch (IOException e) {
// ignore
}
try {
if (m_ServerSocket != null) {
m_ServerSocket.close(1000, "");
}
} catch (Exception e) {
// ignore
}
m_ServerSocket = null;
m_ClientSocket = null;
m_ServerSocketRaw = null;
FileLog.d("Proxy Closed.");
}
public void sendToClient(byte[] buffer) {
sendToClient(buffer, buffer.length);
}
public void sendToClient(byte[] buffer, int len) {
if (m_ClientOutput != null && len > 0 && len <= buffer.length) {
try {
m_ClientOutput.write(buffer, 0, len);
m_ClientOutput.flush();
} catch (IOException e) {
FileLog.e("Sending data to client", e);
}
}
}
public static Object okhttpClient;
public void connectToServer(String server, Runnable succ, Runnable fail) throws IOException {
if (server.equals("")) {
close();
FileLog.e("Invalid Remote Host Name - Empty String !!!");
return;
}
Integer target = mapper.get(server);
for (int i = 1; target == null && i < 4; i++) {
target = mapper.get(server.substring(0, server.length() - i));
}
if (target == null || target.equals(-1)) {
// Too many logs
if (!mapper.containsKey(server)) {
mapper.put(server, -1);
FileLog.e("No route for ip " + server);
}
close();
return;
}
String ip = server;
if (bean.getPayload().size() >= target) {
server = bean.getPayload().get(target - 1);
}
if (BuildConfig.DEBUG) {
FileLog.d("Route " + ip + " to dc" + target + ": " + (bean.getTls() ? "wss://" : "ws://") + server + "." + bean.getServer() + "/api");
}
if (okhttpClient == null) {
okhttpClient = new OkHttpClient.Builder().dns(new WsLoader.CustomDns()).build();
}
((OkHttpClient) okhttpClient)
.newWebSocket(new Request.Builder()
.url((bean.getTls() ? "wss://" : "ws://") + server + "." + bean.getServer() + "/api")
.build(), new WebSocketListener() {
@Override
public void onOpen(@NotNull okhttp3.WebSocket webSocket, @NotNull Response response) {
m_ServerSocket = webSocket;
m_ServerSocketRaw = NativeImageTestsAccessorsKt.getConnection(Objects.requireNonNull(NativeImageTestsAccessorsKt.getExchange(response))).socket();
succ.run();
}
@Override
public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
error = t;
fail.run();
}
@Override
public void onMessage(@NotNull okhttp3.WebSocket webSocket, @NotNull ByteString bytes) {
FileLog.d("[" + webSocket.request().url() + "] Reveived " + bytes.size() + " bytes");
ProxyHandler.this.sendToClient(bytes.toByteArray());
}
@Override
public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
FileLog.d("[" + webSocket.request().url() + "] Reveived text: " + text);
}
@Override
public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
FileLog.d("[" + webSocket.request().url() + "] Closed: " + code + " " + reason);
}
@Override
public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
FileLog.d("[" + webSocket.request().url() + "] Closing: " + code + " " + reason);
close();
}
});
}
public boolean prepareClient() {
if (m_ClientSocket == null) return false;
try {
m_ClientInput = m_ClientSocket.getInputStream();
m_ClientOutput = m_ClientSocket.getOutputStream();
return true;
} catch (IOException e) {
FileLog.e("Proxy - can't get I/O streams!");
FileLog.e(e.getMessage(), e);
return false;
}
}
public void processRelay() {
try {
byte SOCKS_Version = getByteFromClient();
switch (SOCKS_Version) {
case SocksConstants.SOCKS4_Version:
comm = new Socks4Impl(this);
break;
case SocksConstants.SOCKS5_Version:
comm = new Socks5Impl(this);
break;
default:
FileLog.e("Invalid SOKCS version : " + SOCKS_Version);
return;
}
FileLog.d("Accepted SOCKS " + SOCKS_Version + " Request.");
comm.authenticate(SOCKS_Version);
comm.getClientCommand();
if (comm.socksCommand == SocksConstants.SC_CONNECT) {
comm.connect();
relay();
}
} catch (Exception e) {
FileLog.e(e.getMessage(), e);
}
}
public byte getByteFromClient() throws Exception {
while (m_ClientSocket != null) {
int b;
try {
b = m_ClientInput.read();
} catch (InterruptedIOException e) {
Thread.yield();
continue;
}
return (byte) b; // return loaded byte
}
throw new Exception("Interrupted Reading GetByteFromClient()");
}
public void relay() {
for (boolean isActive = true; isActive; Thread.yield()) {
int dlen = this.checkClientData();
if (dlen < 0) {
isActive = false;
}
if (dlen > 0) {
while (m_ServerSocket == null && error == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
return;
}
}
if (error != null) throw new RuntimeException(error);
FileLog.d("[" + m_ServerSocket.request().url() + "] Send " + dlen + " bytes");
this.m_ServerSocket.send(ByteString.of(Arrays.copyOf(this.m_Buffer, dlen)));
}
}
}
public int checkClientData() {
synchronized (m_lock) {
// The client side is not opened.
if (m_ClientInput == null) return -1;
int dlen;
try {
dlen = m_ClientInput.read(m_Buffer, 0, SocksConstants.DEFAULT_BUF_SIZE);
} catch (InterruptedIOException e) {
return 0;
} catch (IOException e) {
FileLog.d("Client connection Closed!");
close(); // Close the server on this exception
return -1;
}
if (dlen < 0) close();
return dlen;
}
}
}

View File

@ -1,162 +0,0 @@
package tw.nekomimi.nekogram.proxy.tcp2ws;
import org.jetbrains.annotations.NotNull;
import org.telegram.messenger.FileLog;
import java.net.InetAddress;
public class Socks4Impl {
final ProxyHandler m_Parent;
final byte[] DST_Port = new byte[2];
byte[] DST_Addr = new byte[4];
byte SOCKS_Version = 0;
byte socksCommand;
InetAddress m_ServerIP = null;
int m_nServerPort = 0;
InetAddress m_ClientIP = null;
int m_nClientPort = 0;
Socks4Impl(ProxyHandler Parent) {
m_Parent = Parent;
}
public byte getSuccessCode() {
return 90;
}
public byte getFailCode() {
return 91;
}
@NotNull
public String commName(byte code) {
switch (code) {
case 0x01:
return "CONNECT";
case 0x02:
return "BIND";
case 0x03:
return "UDP Association";
default:
return "Unknown Command";
}
}
@NotNull
public String replyName(byte code) {
switch (code) {
case 0:
return "SUCCESS";
case 1:
return "General SOCKS Server failure";
case 2:
return "Connection not allowed by ruleset";
case 3:
return "Network Unreachable";
case 4:
return "HOST Unreachable";
case 5:
return "Connection Refused";
case 6:
return "TTL Expired";
case 7:
return "Command not supported";
case 8:
return "Address Type not Supported";
case 9:
return "to 0xFF UnAssigned";
case 90:
return "Request GRANTED";
case 91:
return "Request REJECTED or FAILED";
case 92:
return "Request REJECTED - SOCKS server can't connect to Identd on the client";
case 93:
return "Request REJECTED - Client and Identd report diff user-ID";
default:
return "Unknown Command";
}
}
public boolean isInvalidAddress() {
// IP v4 Address Type
m_ServerIP = Utils.calcInetAddress(DST_Addr);
m_nServerPort = Utils.calcPort(DST_Port[0], DST_Port[1]);
m_ClientIP = m_Parent.m_ClientSocket.getInetAddress();
m_nClientPort = m_Parent.m_ClientSocket.getPort();
return m_ServerIP == null || m_nServerPort < 0;
}
protected byte getByte() {
try {
return m_Parent.getByteFromClient();
} catch (Exception e) {
return 0;
}
}
public void authenticate(byte SOCKS_Ver) throws Exception {
SOCKS_Version = SOCKS_Ver;
}
public void getClientCommand() throws Exception {
// Version was get in method Authenticate()
socksCommand = getByte();
DST_Port[0] = getByte();
DST_Port[1] = getByte();
for (int i = 0; i < 4; i++) {
DST_Addr[i] = getByte();
}
//noinspection StatementWithEmptyBody
while (getByte() != 0x00) {
// keep reading bytes
}
if ((socksCommand < SocksConstants.SC_CONNECT) || (socksCommand > SocksConstants.SC_BIND)) {
refuseCommand((byte) 91);
throw new Exception("Socks 4 - Unsupported Command : " + commName(socksCommand));
}
if (isInvalidAddress()) { // Gets the IP Address
refuseCommand((byte) 92); // Host Not Exists...
throw new Exception("Socks 4 - Unknown Host/IP address '" + m_ServerIP.toString());
}
FileLog.d("Accepted SOCKS 4 Command: \"" + commName(socksCommand) + "\"");
}
public void replyCommand(byte ReplyCode) {
FileLog.d("Socks 4 reply: \"" + replyName(ReplyCode) + "\"");
byte[] REPLY = new byte[8];
REPLY[0] = 0;
REPLY[1] = ReplyCode;
REPLY[2] = DST_Port[0];
REPLY[3] = DST_Port[1];
REPLY[4] = DST_Addr[0];
REPLY[5] = DST_Addr[1];
REPLY[6] = DST_Addr[2];
REPLY[7] = DST_Addr[3];
m_Parent.sendToClient(REPLY);
}
protected void refuseCommand(byte errorCode) {
FileLog.d("Socks 4 - Refuse Command: \"" + replyName(errorCode) + "\"");
replyCommand(errorCode);
}
public void connect() throws Exception {
FileLog.d("Connecting...");
// Connect to the Remote Host
m_Parent.connectToServer(m_ServerIP.getHostAddress(), () -> replyCommand(getSuccessCode()), () -> refuseCommand(getFailCode()));
}
}

View File

@ -1,217 +0,0 @@
package tw.nekomimi.nekogram.proxy.tcp2ws;
import androidx.annotation.Nullable;
import org.telegram.messenger.FileLog;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class Socks5Impl extends Socks4Impl {
private static final int[] ADDR_Size = {
-1, //'00' No such AType
4, //'01' IP v4 - 4Bytes
-1, //'02' No such AType
-1, //'03' First Byte is Len
16 //'04' IP v6 - 16bytes
};
private static final byte[] SRE_REFUSE = {(byte) 0x05, (byte) 0xFF};
private static final byte[] SRE_ACCEPT = {(byte) 0x05, (byte) 0x00};
private static final int MAX_ADDR_LEN = 255;
private byte ADDRESS_TYPE;
private DatagramSocket DGSocket = null;
private DatagramPacket DGPack = null;
private InetAddress UDP_IA = null;
private int UDP_port = 0;
Socks5Impl(ProxyHandler Parent) {
super(Parent);
DST_Addr = new byte[MAX_ADDR_LEN];
}
@SuppressWarnings("OctalInteger")
public byte getSuccessCode() {
return 00;
}
@SuppressWarnings("OctalInteger")
public byte getFailCode() {
return 04;
}
@Nullable
public InetAddress calcInetAddress(byte AType, byte[] addr) {
InetAddress IA;
switch (AType) {
// Version IP 4
case 0x01:
IA = Utils.calcInetAddress(addr);
break;
// Version IP DOMAIN NAME
case 0x03:
if (addr[0] <= 0) {
FileLog.e("SOCKS 5 - calcInetAddress() : BAD IP in command - size : " + addr[0]);
return null;
}
StringBuilder sIA = new StringBuilder();
for (int i = 1; i <= addr[0]; i++) {
sIA.append((char) addr[i]);
}
try {
IA = InetAddress.getByName(sIA.toString());
} catch (UnknownHostException e) {
return null;
}
break;
default:
return null;
}
return IA;
}
public boolean isInvalidAddress() {
m_ServerIP = calcInetAddress(ADDRESS_TYPE, DST_Addr);
m_nServerPort = Utils.calcPort(DST_Port[0], DST_Port[1]);
m_ClientIP = m_Parent.m_ClientSocket.getInetAddress();
m_nClientPort = m_Parent.m_ClientSocket.getPort();
return !((m_ServerIP != null) && (m_nServerPort >= 0));
}
public void authenticate(byte SOCKS_Ver) throws Exception {
super.authenticate(SOCKS_Ver); // Sets SOCKS Version...
if (SOCKS_Version == SocksConstants.SOCKS5_Version) {
if (!checkAuthentication()) {// It reads whole Cli Request
refuseAuthentication("SOCKS 5 - Not Supported Authentication!");
throw new Exception("SOCKS 5 - Not Supported Authentication.");
}
acceptAuthentication();
}// if( SOCKS_Version...
else {
refuseAuthentication("Incorrect SOCKS version : " + SOCKS_Version);
throw new Exception("Not Supported SOCKS Version -'" +
SOCKS_Version + "'");
}
}
public void refuseAuthentication(String msg) {
FileLog.d("SOCKS 5 - Refuse Authentication: '" + msg + "'");
m_Parent.sendToClient(SRE_REFUSE);
}
public void acceptAuthentication() {
FileLog.d("SOCKS 5 - Accepts Auth. method 'NO_AUTH'");
byte[] tSRE_Accept = SRE_ACCEPT;
tSRE_Accept[0] = SOCKS_Version;
m_Parent.sendToClient(tSRE_Accept);
}
public boolean checkAuthentication() {
final byte Methods_Num = getByte();
final StringBuilder Methods = new StringBuilder();
for (int i = 0; i < Methods_Num; i++) {
Methods.append(",-").append(getByte()).append('-');
}
return ((Methods.indexOf("-0-") != -1) || (Methods.indexOf("-00-") != -1));
}
public void getClientCommand() throws Exception {
SOCKS_Version = getByte();
socksCommand = getByte();
/*byte RSV =*/
getByte(); // Reserved. Must be'00'
ADDRESS_TYPE = getByte();
int Addr_Len = ADDR_Size[ADDRESS_TYPE];
DST_Addr[0] = getByte();
if (ADDRESS_TYPE == 0x03) {
Addr_Len = DST_Addr[0] + 1;
}
for (int i = 1; i < Addr_Len; i++) {
DST_Addr[i] = getByte();
}
DST_Port[0] = getByte();
DST_Port[1] = getByte();
if (SOCKS_Version != SocksConstants.SOCKS5_Version) {
FileLog.d("SOCKS 5 - Incorrect SOCKS Version of Command: " +
SOCKS_Version);
refuseCommand((byte) 0xFF);
throw new Exception("Incorrect SOCKS Version of Command: " +
SOCKS_Version);
}
if ((socksCommand < SocksConstants.SC_CONNECT) || (socksCommand > SocksConstants.SC_UDP)) {
FileLog.e("SOCKS 5 - GetClientCommand() - Unsupported Command : \"" + commName(socksCommand) + "\"");
refuseCommand((byte) 0x07);
throw new Exception("SOCKS 5 - Unsupported Command: \"" + socksCommand + "\"");
}
if (ADDRESS_TYPE == 0x04) {
FileLog.e("SOCKS 5 - GetClientCommand() - Unsupported Address Type - IP v6");
refuseCommand((byte) 0x08);
throw new Exception("Unsupported Address Type - IP v6");
}
if ((ADDRESS_TYPE >= 0x04) || (ADDRESS_TYPE <= 0)) {
FileLog.e("SOCKS 5 - GetClientCommand() - Unsupported Address Type: " + ADDRESS_TYPE);
refuseCommand((byte) 0x08);
throw new Exception("SOCKS 5 - Unsupported Address Type: " + ADDRESS_TYPE);
}
if (isInvalidAddress()) { // Gets the IP Address
refuseCommand((byte) 0x04); // Host Not Exists...
throw new Exception("SOCKS 5 - Unknown Host/IP address '" + m_ServerIP.toString() + "'");
}
FileLog.d("SOCKS 5 - Accepted SOCKS5 Command: \"" + commName(socksCommand) + "\"");
}
public void replyCommand(byte replyCode) {
FileLog.d("SOCKS 5 - Reply to Client \"" + replyName(replyCode) + "\"");
final int pt;
byte[] REPLY = new byte[10];
byte[] IP = new byte[4];
if (m_Parent.m_ServerSocketRaw != null) {
pt = m_Parent.m_ServerSocketRaw.getLocalPort();
} else {
IP[0] = 0;
IP[1] = 0;
IP[2] = 0;
IP[3] = 0;
pt = 0;
}
formGenericReply(replyCode, pt, REPLY, IP);
m_Parent.sendToClient(REPLY);// BND.PORT
}
private void formGenericReply(byte replyCode, int pt, byte[] REPLY, byte[] IP) {
REPLY[0] = SocksConstants.SOCKS5_Version;
REPLY[1] = replyCode;
REPLY[2] = 0x00; // Reserved '00'
REPLY[3] = 0x01; // DOMAIN NAME Address Type IP v4
REPLY[4] = IP[0];
REPLY[5] = IP[1];
REPLY[6] = IP[2];
REPLY[7] = IP[3];
REPLY[8] = (byte) ((pt & 0xFF00) >> 8);// Port High
REPLY[9] = (byte) (pt & 0x00FF); // Port Low
}
}

View File

@ -1,18 +0,0 @@
package tw.nekomimi.nekogram.proxy.tcp2ws;
public interface SocksConstants {
// refactor
int LISTEN_TIMEOUT = 2000;
int DEFAULT_SERVER_TIMEOUT = 2000;
int DEFAULT_BUF_SIZE = 4096;
int DEFAULT_PROXY_TIMEOUT = 2000;
byte SOCKS5_Version = 0x05;
byte SOCKS4_Version = 0x04;
byte SC_CONNECT = 0x01;
byte SC_BIND = 0x02;
byte SC_UDP = 0x03;
}

View File

@ -6,54 +6,52 @@ import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collections;
import java.util.HashMap;
import tw.nekomimi.nekogram.proxy.WsLoader;
import java.util.Map;
public class Tcp2wsServer extends Thread {
public WsLoader.Bean bean;
public int port;
public final WsLoader.Bean bean;
public final int port;
public Tcp2wsServer(WsLoader.Bean bean, int port) {
this.bean = bean;
this.port = port;
}
public static final HashMap<String, Integer> mapper = new HashMap<>();
static {
mapper.put("149.154.175.5", 1);
mapper.put("95.161.76.100", 2);
mapper.put("149.154.175.100", 3);
mapper.put("149.154.167.91", 4);
mapper.put("149.154.167.92", 4);
mapper.put("149.154.171.5", 5);
mapper.put("2001:b28:f23d:f001:0000:0000:0000:000a", 1);
mapper.put("2001:67c:4e8:f002:0000:0000:0000:000a", 2);
mapper.put("2001:b28:f23d:f003:0000:0000:0000:000a", 3);
mapper.put("2001:67c:4e8:f004:0000:0000:0000:000a", 4);
mapper.put("2001:b28:f23f:f005:0000:0000:0000:000a", 5);
mapper.put("149.154.161.144", 2);
mapper.put("149.154.167.", 2);
mapper.put("149.154.175.1", 3);
mapper.put("91.108.4.", 4);
mapper.put("149.154.164.", 4);
mapper.put("149.154.165.", 4);
mapper.put("149.154.166.", 4);
mapper.put("91.108.56.", 5);
mapper.put("2001:b28:f23d:f001:0000:0000:0000:000d", 1);
mapper.put("2001:67c:4e8:f002:0000:0000:0000:000d", 2);
mapper.put("2001:b28:f23d:f003:0000:0000:0000:000d", 3);
mapper.put("2001:67c:4e8:f004:0000:0000:0000:000d", 4);
mapper.put("2001:b28:f23f:f005:0000:0000:0000:000d", 5);
mapper.put("149.154.175.40", 6);
mapper.put("149.154.167.40", 7);
mapper.put("149.154.175.117", 8);
mapper.put("2001:b28:f23d:f001:0000:0000:0000:000e", 6);
mapper.put("2001:67c:4e8:f002:0000:0000:0000:000e", 7);
mapper.put("2001:b28:f23d:f003:0000:0000:0000:000e", 8);
}
public static final Map<String, Integer> mapper = Collections.unmodifiableMap(new HashMap<String, Integer>() {{
put("149.154.175.5", 1);
put("95.161.76.100", 2);
put("149.154.175.100", 3);
put("149.154.167.91", 4);
put("149.154.167.92", 4);
put("149.154.171.5", 5);
put("2001:b28:f23d:f001:0000:0000:0000:000a", 1);
put("2001:67c:4e8:f002:0000:0000:0000:000a", 2);
put("2001:b28:f23d:f003:0000:0000:0000:000a", 3);
put("2001:67c:4e8:f004:0000:0000:0000:000a", 4);
put("2001:b28:f23f:f005:0000:0000:0000:000a", 5);
put("149.154.161.144", 2);
put("149.154.167.", 2);
put("149.154.175.1", 3);
put("91.108.4.", 4);
put("149.154.164.", 4);
put("149.154.165.", 4);
put("149.154.166.", 4);
put("91.108.56.", 5);
put("2001:b28:f23d:f001:0000:0000:0000:000d", 1);
put("2001:67c:4e8:f002:0000:0000:0000:000d", 2);
put("2001:b28:f23d:f003:0000:0000:0000:000d", 3);
put("2001:67c:4e8:f004:0000:0000:0000:000d", 4);
put("2001:b28:f23f:f005:0000:0000:0000:000d", 5);
put("149.154.175.40", 6);
put("149.154.167.40", 7);
put("149.154.175.117", 8);
put("2001:b28:f23d:f001:0000:0000:0000:000e", 6);
put("2001:67c:4e8:f002:0000:0000:0000:000e", 7);
put("2001:b28:f23d:f003:0000:0000:0000:000e", 8);
}});
@Override
public void run() {
@ -61,39 +59,33 @@ public class Tcp2wsServer extends Thread {
try {
handleClients(port);
FileLog.d("SOCKS server stopped...");
} catch (IOException e) {
} catch (Exception e) {
FileLog.d("SOCKS server crashed...");
FileLog.e(e);
interrupt();
}
}
protected void handleClients(int port) throws IOException {
protected void handleClients(int port) throws Exception {
final ServerSocket listenSocket = new ServerSocket(port);
listenSocket.setSoTimeout(SocksConstants.LISTEN_TIMEOUT);
Tcp2wsServer.this.port = listenSocket.getLocalPort();
listenSocket.setSoTimeout(2000);
FileLog.d("SOCKS server listening at port: " + listenSocket.getLocalPort());
while (isAlive() && !isInterrupted()) {
handleNextClient(listenSocket);
try {
final Socket clientSocket = listenSocket.accept();
FileLog.d("Connection from : " + clientSocket.getRemoteSocketAddress().toString());
new WsProxyHandler(clientSocket, bean).start();
} catch (InterruptedIOException e) {
// This exception is thrown when accept timeout is expired
} catch (Exception e) {
FileLog.e(e.getMessage(), e);
}
}
try {
listenSocket.close();
} catch (IOException e) {
// ignore
}
}
private void handleNextClient(ServerSocket listenSocket) {
try {
final Socket clientSocket = listenSocket.accept();
clientSocket.setSoTimeout(SocksConstants.DEFAULT_SERVER_TIMEOUT);
FileLog.d("Connection from : " + Utils.getSocketInfo(clientSocket));
new Thread(new ProxyHandler(clientSocket, mapper, bean)).start();
} catch (InterruptedIOException e) {
// This exception is thrown when accept timeout is expired
} catch (Exception e) {
FileLog.e(e.getMessage(), e);
FileLog.e(e);
}
}
}

View File

@ -1,72 +0,0 @@
package tw.nekomimi.nekogram.proxy.tcp2ws;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import static java.lang.String.format;
public final class Utils {
private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
@Nullable
public static InetAddress calcInetAddress(byte[] addr) {
InetAddress IA;
StringBuilder sIA = new StringBuilder();
if (addr.length < 4) {
LOGGER.error("calcInetAddress() - Invalid length of IP v4 - " + addr.length + " bytes");
return null;
}
// IP v4 Address Type
for (int i = 0; i < 4; i++) {
sIA.append(byte2int(addr[i]));
if (i < 3) sIA.append(".");
}
try {
IA = InetAddress.getByName(sIA.toString());
} catch (UnknownHostException e) {
return null;
}
return IA;
}
public static int byte2int(byte b) {
return (int) b < 0 ? 0x100 + (int) b : b;
}
public static int calcPort(byte Hi, byte Lo) {
return ((byte2int(Hi) << 8) | byte2int(Lo));
}
@NotNull
public static String iP2Str(InetAddress IP) {
return IP == null
? "NA/NA"
: format("%s/%s", IP.getHostName(), IP.getHostAddress());
}
@NotNull
public static String getSocketInfo(Socket sock) {
return sock == null
? "<NA/NA:0>"
: format("<%s:%d>", Utils.iP2Str(sock.getInetAddress()), sock.getPort());
}
@NotNull
public static String getSocketInfo(DatagramPacket DGP) {
return DGP == null
? "<NA/NA:0>"
: format("<%s:%d>", Utils.iP2Str(DGP.getAddress()), DGP.getPort());
}
}

View File

@ -1,11 +1,10 @@
package tw.nekomimi.nekogram.proxy
package tw.nekomimi.nekogram.proxy.tcp2ws
import cn.hutool.core.codec.Base64
import cn.hutool.core.util.StrUtil
import okhttp3.Dns
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import tw.nekomimi.nekogram.proxy.tcp2ws.Tcp2wsServer
import tw.nekomimi.nekogram.NekoConfig
import java.net.InetAddress
@ -71,21 +70,4 @@ class WsLoader {
}
}
// For OKHttp in ProxyHandler.java
class CustomDns : Dns {
override fun lookup(hostname: String): List<InetAddress> {
val list = ArrayList<InetAddress>()
val ip = NekoConfig.customPublicProxyIP.String()
if (StrUtil.isBlank(ip)) {
return Dns.SYSTEM.lookup(hostname)
}
return try {
list.add(InetAddress.getByName(ip))
list
} catch (e: Exception) {
Dns.SYSTEM.lookup(hostname)
}
}
}
}

View File

@ -0,0 +1,287 @@
package tw.nekomimi.nekogram.proxy.tcp2ws;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.telegram.messenger.FileLog;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
import tw.nekomimi.nekogram.NekoConfig;
public class WsProxyHandler extends Thread {
private InputStream clientInputStream = null;
private OutputStream clientOutputStream = null;
private final WsLoader.Bean bean;
private Socket clientSocket;
private WebSocket wsSocket = null;
private final byte[] buffer = new byte[4096];
private final AtomicInteger wsStatus = new AtomicInteger(0);
private final static int STATUS_OPENED = 1;
private final static int STATUS_CLOSED = 2;
private final static int STATUS_FAILED = 3;
public WsProxyHandler(Socket clientSocket, WsLoader.Bean bean) {
this.bean = bean;
this.clientSocket = clientSocket;
FileLog.d("ProxyHandler Created.");
}
@Override
public void run() {
FileLog.d("Proxy Started.");
try {
clientInputStream = clientSocket.getInputStream();
clientOutputStream = clientSocket.getOutputStream();
// Handle Socks5 HandShake
socks5Handshake();
FileLog.d("socks5 handshake and websocket connection done");
// Start read from client socket and send to websocket
while (clientSocket != null && wsSocket != null && wsStatus.get() == STATUS_OPENED && !clientSocket.isClosed() && !clientSocket.isInputShutdown()) {
int readLen = this.clientInputStream.read(buffer);
FileLog.d(String.format("read %d from client", readLen));
if (readLen == -1) {
close();
return;
}
if (readLen < 10) {
FileLog.d(Arrays.toString(Arrays.copyOf(buffer, readLen)));
}
this.wsSocket.send(ByteString.of(Arrays.copyOf(buffer, readLen)));
}
} catch (SocketException se) {
if ("Socket closed".equals(se.getMessage())) {
FileLog.d("socket closed from ws when reading from client");
close();
} else {
FileLog.e(se);
close();
}
}
catch (Exception e) {
FileLog.e(e);
close();
}
}
public void close() {
int cur = wsStatus.get();
if (cur == STATUS_CLOSED)
return;
wsStatus.set(STATUS_CLOSED);
FileLog.d("ws handler closed");
try {
if (clientSocket != null)
clientSocket.close();
} catch (IOException e) {
// ignore
}
try {
if (wsSocket != null) {
wsSocket.close(1000, "");
}
} catch (Exception e) {
// ignore
}
clientSocket = null;
wsSocket = null;
}
private static OkHttpClient okhttpClient = null;
private static final Object okhttpLock = new Object();
private static OkHttpClient getOkHttpClientInstance() {
if (okhttpClient == null) {
synchronized (okhttpLock) {
if (okhttpClient == null) {
okhttpClient = new OkHttpClient.Builder()
.dns(s -> {
ArrayList<InetAddress> ret = new ArrayList<>();
FileLog.d("okhttpWS: resolving: " + s);
if (StringUtils.isNotBlank(NekoConfig.customPublicProxyIP.String())) {
ret.add(InetAddress.getByName(NekoConfig.customPublicProxyIP.String()));
} else
ret.addAll(Arrays.asList(InetAddress.getAllByName(s)));
FileLog.d("okhttpWS: resolved: " + ret.toString());
return ret;
})
.build();
}
}
}
return okhttpClient;
}
private void connectToServer(String wsHost) {
getOkHttpClientInstance()
.newWebSocket(new Request.Builder()
.url((bean.getTls() ? "wss://" : "ws://") + wsHost + "/api")
.build(), new WebSocketListener() {
@Override
public void onOpen(@NotNull okhttp3.WebSocket webSocket, @NotNull Response response) {
WsProxyHandler.this.wsSocket = webSocket;
wsStatus.set(STATUS_OPENED);
synchronized (wsStatus) {
wsStatus.notify();
}
}
@Override
public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
FileLog.e(t);
wsStatus.set(STATUS_FAILED);
synchronized (wsStatus) {
wsStatus.notify();
}
WsProxyHandler.this.close();
}
@Override
public void onMessage(@NotNull okhttp3.WebSocket webSocket, @NotNull ByteString bytes) {
FileLog.d("[" + wsHost + "] Received " + bytes.size() + " bytes");
try {
if (wsStatus.get() == STATUS_OPENED && !WsProxyHandler.this.clientSocket.isOutputShutdown())
WsProxyHandler.this.clientOutputStream.write(bytes.toByteArray());
} catch (IOException e) {
FileLog.e(e);
WsProxyHandler.this.close();
}
}
@Override
public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
FileLog.d("[" + wsHost + "] Received text: " + text);
}
@Override
public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
FileLog.d("[" + wsHost + "] Closed: " + code + " " + reason);
WsProxyHandler.this.close();
synchronized (wsStatus) {
wsStatus.notify();
}
}
@Override
public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
FileLog.d("[" + wsHost + "] Closing: " + code + " " + reason);
WsProxyHandler.this.close();
synchronized (wsStatus) {
wsStatus.notify();
}
}
});
}
private static final byte[] RESP_AUTH = new byte[]{0x05, 0x00};
private static final byte[] RESP_SUCCESS = new byte[]{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
private static final byte[] RESP_FAILED = new byte[]{0x05, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
private void socks5Handshake() throws Exception {
byte socksVersion = readOneByteFromClient();
if (socksVersion != 0x05) {
throw new Exception("Invalid socks version:" + socksVersion);
}
FileLog.d("Accepted socks5 requests.");
byte authMethodsLen = readOneByteFromClient();
boolean isNoAuthSupport = false;
for (int i = 0; i < authMethodsLen; i++) {
byte authMethod = readOneByteFromClient();
if (authMethod == 0x00)
isNoAuthSupport = true;
}
if (!isNoAuthSupport) throw new Exception("NO_AUTH is not supported from client.");
this.clientOutputStream.write(RESP_AUTH);
this.clientOutputStream.flush();
byte[] cmds = readBytesExactly(4);
// cmds[0] -> VER
// cmds[1] -> CMD
// cmds[2] -> RSV
// cmds[3] -> ADDR_TYPE
if (cmds[0] != 0x05 || cmds[1] != 0x01 || cmds[2] != 0x00)
throw new Exception("invalid socks5 cmds " + Arrays.toString(cmds));
int addrType = cmds[3];
String address;
if (addrType == 0x01) { // ipv4
address = InetAddress.getByAddress(readBytesExactly(4)).getHostAddress();
} else if (addrType == 0x04) { // ipv6
address = Inet6Address.getByAddress(readBytesExactly(16)).getHostAddress();
} else { // not supported: domain
throw new Exception("invalid addr type: " + addrType);
}
readBytesExactly(2); // read out port
String wsHost = getWsHost(address);
connectToServer(wsHost);
synchronized (wsStatus) {
wsStatus.wait();
}
if (wsStatus.get() == STATUS_OPENED) {
this.clientOutputStream.write(RESP_SUCCESS);
this.clientOutputStream.flush();
} else {
this.clientOutputStream.write(RESP_FAILED);
this.clientOutputStream.flush();
throw new Exception("websocket connect failed");
}
// just set status byte and ignore bnd.addr and bnd.port in RFC1928, since Telegram Android ignores it:
// proxyAuthState == 6 in tgnet/ConnectionSocket.cpp
}
private String getWsHost(String address) throws Exception {
Integer dcNumber = Tcp2wsServer.mapper.get(address);
for (int i = 1; dcNumber == null && i < 4; i++) {
dcNumber = Tcp2wsServer.mapper.get(address.substring(0, address.length() - i));
}
if (dcNumber == null)
throw new Exception("no matched dc: " + address);
if (dcNumber >= bean.getPayload().size())
throw new Exception("invalid dc number & payload: " + dcNumber);
String serverPrefix = bean.getPayload().get(dcNumber - 1);
String wsHost = serverPrefix + "." + this.bean.getServer();
FileLog.d("socks5 dest address: " + address + ", target ws host " + wsHost);
return wsHost;
}
private byte readOneByteFromClient() throws Exception {
return (byte) clientInputStream.read();
}
private byte[] readBytesExactly(int len) throws Exception {
byte[] ret = new byte[len];
int alreadyRead = 0;
while (alreadyRead < len) {
int read = this.clientInputStream.read(ret, alreadyRead, len - alreadyRead);
alreadyRead += read;
}
return ret;
}
}