1
0
mirror of https://github.com/MGislv/NekoX.git synced 2024-06-30 10:14:04 +00:00

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.ShadowsocksLoader;
import tw.nekomimi.nekogram.proxy.ShadowsocksRLoader; import tw.nekomimi.nekogram.proxy.ShadowsocksRLoader;
import tw.nekomimi.nekogram.proxy.VmessLoader; 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.SubInfo;
import tw.nekomimi.nekogram.proxy.SubManager; import tw.nekomimi.nekogram.proxy.SubManager;
import tw.nekomimi.nekogram.utils.AlertUtil; import tw.nekomimi.nekogram.utils.AlertUtil;

View File

@ -912,29 +912,19 @@ public class LaunchActivity extends BasePermissionsActivity implements ActionBar
MediaController.getInstance().setBaseActivity(this, true); MediaController.getInstance().setBaseActivity(this, true);
UIUtil.runOnIoDispatcher(() -> { UIUtil.runOnIoDispatcher(() -> {
// ExternalGcm.checkUpdate(this);
ExternalGcm.checkUpdate(this); if (NekoConfig.autoUpdateSubInfo.Bool())
for (SubInfo subInfo : SubManager.getSubList().find()) {
if (NekoConfig.autoUpdateSubInfo.Bool()) for (SubInfo subInfo : SubManager.getSubList().find()) { if (subInfo == null || !subInfo.enable) continue;
try {
if (subInfo == null || !subInfo.enable) continue; subInfo.proxies = subInfo.reloadProxies();
subInfo.lastFetch = System.currentTimeMillis();
try { SubManager.getSubList().update(subInfo, true);
SharedConfig.reloadProxyList();
subInfo.proxies = subInfo.reloadProxies(); } catch (IOException allTriesFailed) {
subInfo.lastFetch = System.currentTimeMillis(); FileLog.e(allTriesFailed);
}
SubManager.getSubList().update(subInfo, true);
SharedConfig.reloadProxyList();
} catch (IOException allTriesFailed) {
FileLog.e(allTriesFailed);
} }
}
}); });
//FileLog.d("UI create time = " + (SystemClock.elapsedRealtime() - ApplicationLoader.startTime)); //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() return emptyList()
// Try DoH first ( github.com is often blocked // Try DoH first ( github.com is often blocked
try { try {
var content = DnsFactory.getTxts("nachonekodayo.sekai.icu").joinToString() val content = DnsFactory.getTxts("nachonekodayo.sekai.icu").joinToString()
val proxiesString = StrUtil.getSubString(content, "#NekoXStart#", "#NekoXEnd#") val proxiesString = StrUtil.getSubString(content, "#NekoXStart#", "#NekoXEnd#")
if (proxiesString.equals(content)) { if (proxiesString.equals(content)) {
throw Exception("DoH get public proxy: Not found") throw Exception("DoH get public proxy: Not found")
} }
val proxies = parseProxies(proxiesString) return parseProxies(proxiesString)
if (proxies.count() == 0) {
throw Exception("DoH get public proxy: Empty")
}
return proxies
} catch (e: Exception) { } catch (e: Exception) {
FileLog.e(e.stackTraceToString()) FileLog.e(e)
} }
// Try Other Urls // Try Other Urls

View File

@ -43,6 +43,7 @@ import org.telegram.ui.Components.LayoutHelper;
import java.util.ArrayList; import java.util.ArrayList;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import tw.nekomimi.nekogram.proxy.tcp2ws.WsLoader;
public class WsSettingsActivity extends BaseFragment { 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.io.InterruptedIOException;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import tw.nekomimi.nekogram.proxy.WsLoader;
public class Tcp2wsServer extends Thread { public class Tcp2wsServer extends Thread {
public WsLoader.Bean bean; public final WsLoader.Bean bean;
public int port; public final int port;
public Tcp2wsServer(WsLoader.Bean bean, int port) { public Tcp2wsServer(WsLoader.Bean bean, int port) {
this.bean = bean; this.bean = bean;
this.port = port; this.port = port;
} }
public static final HashMap<String, Integer> mapper = new HashMap<>(); public static final Map<String, Integer> mapper = Collections.unmodifiableMap(new HashMap<String, Integer>() {{
put("149.154.175.5", 1);
static { put("95.161.76.100", 2);
mapper.put("149.154.175.5", 1); put("149.154.175.100", 3);
mapper.put("95.161.76.100", 2); put("149.154.167.91", 4);
mapper.put("149.154.175.100", 3); put("149.154.167.92", 4);
mapper.put("149.154.167.91", 4); put("149.154.171.5", 5);
mapper.put("149.154.167.92", 4); put("2001:b28:f23d:f001:0000:0000:0000:000a", 1);
mapper.put("149.154.171.5", 5); put("2001:67c:4e8:f002:0000:0000:0000:000a", 2);
mapper.put("2001:b28:f23d:f001:0000:0000:0000:000a", 1); put("2001:b28:f23d:f003:0000:0000:0000:000a", 3);
mapper.put("2001:67c:4e8:f002:0000:0000:0000:000a", 2); put("2001:67c:4e8:f004:0000:0000:0000:000a", 4);
mapper.put("2001:b28:f23d:f003:0000:0000:0000:000a", 3); put("2001:b28:f23f:f005:0000:0000:0000:000a", 5);
mapper.put("2001:67c:4e8:f004:0000:0000:0000:000a", 4); put("149.154.161.144", 2);
mapper.put("2001:b28:f23f:f005:0000:0000:0000:000a", 5); put("149.154.167.", 2);
mapper.put("149.154.161.144", 2); put("149.154.175.1", 3);
mapper.put("149.154.167.", 2); put("91.108.4.", 4);
mapper.put("149.154.175.1", 3); put("149.154.164.", 4);
mapper.put("91.108.4.", 4); put("149.154.165.", 4);
mapper.put("149.154.164.", 4); put("149.154.166.", 4);
mapper.put("149.154.165.", 4); put("91.108.56.", 5);
mapper.put("149.154.166.", 4); put("2001:b28:f23d:f001:0000:0000:0000:000d", 1);
mapper.put("91.108.56.", 5); put("2001:67c:4e8:f002:0000:0000:0000:000d", 2);
mapper.put("2001:b28:f23d:f001:0000:0000:0000:000d", 1); put("2001:b28:f23d:f003:0000:0000:0000:000d", 3);
mapper.put("2001:67c:4e8:f002:0000:0000:0000:000d", 2); put("2001:67c:4e8:f004:0000:0000:0000:000d", 4);
mapper.put("2001:b28:f23d:f003:0000:0000:0000:000d", 3); put("2001:b28:f23f:f005:0000:0000:0000:000d", 5);
mapper.put("2001:67c:4e8:f004:0000:0000:0000:000d", 4); put("149.154.175.40", 6);
mapper.put("2001:b28:f23f:f005:0000:0000:0000:000d", 5); put("149.154.167.40", 7);
mapper.put("149.154.175.40", 6); put("149.154.175.117", 8);
mapper.put("149.154.167.40", 7); put("2001:b28:f23d:f001:0000:0000:0000:000e", 6);
mapper.put("149.154.175.117", 8); put("2001:67c:4e8:f002:0000:0000:0000:000e", 7);
mapper.put("2001:b28:f23d:f001:0000:0000:0000:000e", 6); put("2001:b28:f23d:f003:0000:0000:0000:000e", 8);
mapper.put("2001:67c:4e8:f002:0000:0000:0000:000e", 7); }});
mapper.put("2001:b28:f23d:f003:0000:0000:0000:000e", 8);
}
@Override @Override
public void run() { public void run() {
@ -61,39 +59,33 @@ public class Tcp2wsServer extends Thread {
try { try {
handleClients(port); handleClients(port);
FileLog.d("SOCKS server stopped..."); FileLog.d("SOCKS server stopped...");
} catch (IOException e) { } catch (Exception e) {
FileLog.d("SOCKS server crashed..."); FileLog.d("SOCKS server crashed...");
FileLog.e(e);
interrupt(); interrupt();
} }
} }
protected void handleClients(int port) throws IOException { protected void handleClients(int port) throws Exception {
final ServerSocket listenSocket = new ServerSocket(port); final ServerSocket listenSocket = new ServerSocket(port);
listenSocket.setSoTimeout(SocksConstants.LISTEN_TIMEOUT); listenSocket.setSoTimeout(2000);
Tcp2wsServer.this.port = listenSocket.getLocalPort();
FileLog.d("SOCKS server listening at port: " + listenSocket.getLocalPort()); FileLog.d("SOCKS server listening at port: " + listenSocket.getLocalPort());
while (isAlive() && !isInterrupted()) { 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 { try {
listenSocket.close(); listenSocket.close();
} catch (IOException e) { } catch (IOException e) {
// ignore FileLog.e(e);
}
}
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);
} }
} }
} }

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.codec.Base64
import cn.hutool.core.util.StrUtil import cn.hutool.core.util.StrUtil
import okhttp3.Dns import okhttp3.Dns
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import tw.nekomimi.nekogram.proxy.tcp2ws.Tcp2wsServer
import tw.nekomimi.nekogram.NekoConfig import tw.nekomimi.nekogram.NekoConfig
import java.net.InetAddress 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;
}
}