mirror of
https://github.com/MGislv/NekoX.git
synced 2024-07-04 11:13:36 +00:00
OSS WebSocket relay implementation
This commit is contained in:
parent
1623657bba
commit
95c574f012
|
@ -397,6 +397,7 @@ dependencies {
|
|||
implementation "cn.hutool:hutool-crypto:5.6.2"
|
||||
implementation "cn.hutool:hutool-http:5.6.2"
|
||||
implementation "com.jakewharton:process-phoenix:2.0.0"
|
||||
//implementation "com.neovisionaries:nv-websocket-client:2.14"
|
||||
|
||||
compileOnly "org.yaml:snakeyaml:1.28"
|
||||
fullImplementation "org.yaml:snakeyaml:1.28"
|
||||
|
|
|
@ -210,6 +210,8 @@
|
|||
<data android:scheme="vmess1" />
|
||||
<data android:scheme="ss" />
|
||||
<data android:scheme="ssr" />
|
||||
<data android:scheme="ws" />
|
||||
<data android:scheme="wss" />
|
||||
<data android:scheme="trojan" />
|
||||
|
||||
</intent-filter>
|
||||
|
|
|
@ -17,6 +17,8 @@ object V2RayConfig {
|
|||
const val SS_PROTOCOL: String = "ss://"
|
||||
const val SSR_PROTOCOL: String = "ssr://"
|
||||
const val TROJAN_PROTOCOL: String = "trojan://"
|
||||
const val WS_PROTOCOL: String = "ws://"
|
||||
const val WSS_PROTOCOL: String = "wss://"
|
||||
|
||||
const val SOCKS_PROTOCOL: String = "socks://"
|
||||
const val BROADCAST_ACTION_SERVICE = "com.v2ray.ang.action.service"
|
||||
|
|
|
@ -60,7 +60,6 @@ import android.text.method.LinkMovementMethod;
|
|||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.StateSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.ContextThemeWrapper;
|
||||
|
@ -164,6 +163,8 @@ import static com.v2ray.ang.V2RayConfig.SS_PROTOCOL;
|
|||
import static com.v2ray.ang.V2RayConfig.TROJAN_PROTOCOL;
|
||||
import static com.v2ray.ang.V2RayConfig.VMESS1_PROTOCOL;
|
||||
import static com.v2ray.ang.V2RayConfig.VMESS_PROTOCOL;
|
||||
import static com.v2ray.ang.V2RayConfig.WSS_PROTOCOL;
|
||||
import static com.v2ray.ang.V2RayConfig.WS_PROTOCOL;
|
||||
|
||||
public class AndroidUtilities {
|
||||
|
||||
|
@ -440,7 +441,7 @@ public class AndroidUtilities {
|
|||
}
|
||||
}
|
||||
final ArrayList<LinkSpec> links = new ArrayList<>();
|
||||
gatherLinks(links, text, LinkifyPort.PROXY_PATTERN, new String[]{VMESS_PROTOCOL, VMESS1_PROTOCOL, SS_PROTOCOL, SSR_PROTOCOL, TROJAN_PROTOCOL/*, RB_PROTOCOL*/}, sUrlMatchFilter, false);
|
||||
gatherLinks(links, text, LinkifyPort.PROXY_PATTERN, new String[]{VMESS_PROTOCOL, VMESS1_PROTOCOL, SS_PROTOCOL, SSR_PROTOCOL, TROJAN_PROTOCOL, WS_PROTOCOL, WSS_PROTOCOL,/*, RB_PROTOCOL*/}, sUrlMatchFilter, false);
|
||||
pruneOverlaps(links);
|
||||
if (links.size() == 0) {
|
||||
return false;
|
||||
|
@ -2968,6 +2969,8 @@ public class AndroidUtilities {
|
|||
link.startsWith(VMESS1_PROTOCOL) ||
|
||||
link.startsWith(SS_PROTOCOL) ||
|
||||
link.startsWith(SSR_PROTOCOL) ||
|
||||
link.startsWith(WS_PROTOCOL) ||
|
||||
link.startsWith(WSS_PROTOCOL) ||
|
||||
link.startsWith(TROJAN_PROTOCOL)/*||
|
||||
data.startsWith(RB_PROTOCOL)*/) {
|
||||
return ProxyUtil.importProxy(activity, link);
|
||||
|
@ -3599,6 +3602,112 @@ public class AndroidUtilities {
|
|||
builder.show();
|
||||
}
|
||||
|
||||
public static void showWsAlert(Context activity, final SharedConfig.WsProxy info) {
|
||||
BottomSheet.Builder builder = new BottomSheet.Builder(activity);
|
||||
final Runnable dismissRunnable = builder.getDismissRunnable();
|
||||
|
||||
builder.setApplyTopPadding(false);
|
||||
builder.setApplyBottomPadding(false);
|
||||
LinearLayout linearLayout = new LinearLayout(activity);
|
||||
builder.setCustomView(linearLayout);
|
||||
linearLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
for (int a = 0; a < 4; a++) {
|
||||
String text = null;
|
||||
String detail = null;
|
||||
if (a == 0) {
|
||||
text = info.bean.getServer();
|
||||
detail = LocaleController.getString("UseProxyAddress", R.string.UseProxyAddress);
|
||||
} else if (a == 1) {
|
||||
text = info.bean.getTls() ? "Y" : "N";
|
||||
detail = LocaleController.getString("UseProxyPort", R.string.VmessTls);
|
||||
} else {
|
||||
text = LocaleController.getString("Checking", R.string.Checking);
|
||||
detail = LocaleController.getString("Checking", R.string.Checking);
|
||||
}
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
continue;
|
||||
}
|
||||
TextDetailSettingsCell cell = new TextDetailSettingsCell(activity);
|
||||
cell.setTextAndValue(text, detail, true);
|
||||
cell.getTextView().setTextColor(Theme.getColor(Theme.key_dialogTextBlack));
|
||||
cell.getValueTextView().setTextColor(Theme.getColor(Theme.key_dialogTextGray3));
|
||||
linearLayout.addView(cell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT));
|
||||
AtomicInteger count = new AtomicInteger();
|
||||
if (a == 3) {
|
||||
|
||||
RequestTimeDelegate callback = new RequestTimeDelegate() {
|
||||
@Override
|
||||
public void run(long time) {
|
||||
int c = count.getAndIncrement();
|
||||
String colorKey;
|
||||
if (time != -1) {
|
||||
info.stop();
|
||||
cell.setTextAndValue(LocaleController.getString("Available", R.string.Available), LocaleController.formatString("Ping", R.string.Ping, time), true);
|
||||
colorKey = Theme.key_windowBackgroundWhiteGreenText;
|
||||
} else if (c < 2) {
|
||||
ConnectionsManager.getInstance(UserConfig.selectedAccount).checkProxy(info.address, info.port, "", "", "", t -> AndroidUtilities.runOnUIThread(() -> run(t), 500));
|
||||
colorKey = Theme.key_windowBackgroundWhiteGreenText;
|
||||
} else {
|
||||
info.stop();
|
||||
cell.setTextAndValue(LocaleController.getString("Unavailable", R.string.Unavailable), LocaleController.getString("Unavailable", R.string.Unavailable), true);
|
||||
colorKey = Theme.key_windowBackgroundWhiteRedText4;
|
||||
}
|
||||
cell.getValueTextView().setTextColor(Theme.getColor(colorKey));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
UIUtil.runOnIoDispatcher(() -> {
|
||||
|
||||
try {
|
||||
info.start();
|
||||
ConnectionsManager.getInstance(UserConfig.selectedAccount).checkProxy(info.address, info.port, "", "", "", time -> AndroidUtilities.runOnUIThread(() -> callback.run(time)));
|
||||
} catch (Exception e) {
|
||||
FileLog.e(e);
|
||||
AlertUtil.showToast(e);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
PickerBottomLayout pickerBottomLayout = new PickerBottomLayout(activity, false);
|
||||
pickerBottomLayout.setBackgroundColor(Theme.getColor(Theme.key_dialogBackground));
|
||||
linearLayout.addView(pickerBottomLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, 48, Gravity.LEFT | Gravity.BOTTOM));
|
||||
pickerBottomLayout.cancelButton.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0);
|
||||
pickerBottomLayout.cancelButton.setTextColor(Theme.getColor(Theme.key_dialogTextBlue2));
|
||||
pickerBottomLayout.cancelButton.setText(LocaleController.getString("Cancel", R.string.Cancel).toUpperCase());
|
||||
pickerBottomLayout.cancelButton.setOnClickListener(view -> {
|
||||
info.stop();
|
||||
dismissRunnable.run();
|
||||
});
|
||||
pickerBottomLayout.doneButtonTextView.setTextColor(Theme.getColor(Theme.key_dialogTextBlue2));
|
||||
pickerBottomLayout.doneButton.setPadding(AndroidUtilities.dp(18), 0, AndroidUtilities.dp(18), 0);
|
||||
pickerBottomLayout.doneButtonBadgeTextView.setVisibility(View.GONE);
|
||||
pickerBottomLayout.middleButtonTextView.setText(LocaleController.getString("Save", R.string.Save).toUpperCase());
|
||||
pickerBottomLayout.middleButton.setVisibility(View.VISIBLE);
|
||||
pickerBottomLayout.middleButton.setOnClickListener((it) -> {
|
||||
SharedConfig.addProxy(info);
|
||||
|
||||
NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged);
|
||||
|
||||
dismissRunnable.run();
|
||||
|
||||
});
|
||||
pickerBottomLayout.doneButtonTextView.setText(LocaleController.getString("ConnectingConnectProxy", R.string.ConnectingConnectProxy).toUpperCase());
|
||||
pickerBottomLayout.doneButton.setOnClickListener(v -> {
|
||||
|
||||
SharedConfig.setCurrentProxy(SharedConfig.addProxy(info));
|
||||
|
||||
NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.proxySettingsChanged);
|
||||
|
||||
dismissRunnable.run();
|
||||
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
public static String getSystemProperty(String key) {
|
||||
try {
|
||||
|
|
|
@ -156,7 +156,7 @@ public class LinkifyPort {
|
|||
private static final String DOMAIN_NAME_STR = "(" + HOST_NAME + "|" + IP_ADDRESS_STRING + ")";
|
||||
private static final Pattern DOMAIN_NAME = Pattern.compile(DOMAIN_NAME_STR);
|
||||
private static final String PROTOCOL = "(?i:http|https|ton|tg)://";
|
||||
private static final String PROXY_PROTOCOL = "(?i:vmess|vmess1|ss|ssr|trojan)://";
|
||||
private static final String PROXY_PROTOCOL = "(?i:vmess|vmess1|ss|ssr|trojan|ws|wss)://";
|
||||
private static final String WORD_BOUNDARY = "(?:\\b|$|^)";
|
||||
private static final String USER_INFO = "(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
|
||||
+ "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
|
||||
|
|
|
@ -49,6 +49,7 @@ import tw.nekomimi.nekogram.ProxyManager;
|
|||
import tw.nekomimi.nekogram.ShadowsocksLoader;
|
||||
import tw.nekomimi.nekogram.ShadowsocksRLoader;
|
||||
import tw.nekomimi.nekogram.VmessLoader;
|
||||
import tw.nekomimi.nekogram.WsLoader;
|
||||
import tw.nekomimi.nekogram.sub.SubInfo;
|
||||
import tw.nekomimi.nekogram.sub.SubManager;
|
||||
import tw.nekomimi.nekogram.utils.AlertUtil;
|
||||
|
@ -58,6 +59,8 @@ import tw.nekomimi.nekogram.utils.UIUtil;
|
|||
|
||||
import static com.v2ray.ang.V2RayConfig.SSR_PROTOCOL;
|
||||
import static com.v2ray.ang.V2RayConfig.SS_PROTOCOL;
|
||||
import static com.v2ray.ang.V2RayConfig.WSS_PROTOCOL;
|
||||
import static com.v2ray.ang.V2RayConfig.WS_PROTOCOL;
|
||||
|
||||
public class SharedConfig {
|
||||
|
||||
|
@ -337,14 +340,6 @@ public class SharedConfig {
|
|||
|
||||
}
|
||||
|
||||
public JSONObject toJson() throws JSONException {
|
||||
|
||||
JSONObject object = toJsonInternal();
|
||||
|
||||
return object;
|
||||
|
||||
}
|
||||
|
||||
public JSONObject toJsonInternal() throws JSONException {
|
||||
|
||||
JSONObject obj = new JSONObject();
|
||||
|
@ -444,6 +439,14 @@ public class SharedConfig {
|
|||
|
||||
}
|
||||
|
||||
case "ws": {
|
||||
|
||||
info = new WsProxy(obj.optString("link"));
|
||||
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
default: {
|
||||
|
||||
throw new IllegalStateException("invalid proxy type " + obj.optString("type", "null"));
|
||||
|
@ -888,6 +891,79 @@ public class SharedConfig {
|
|||
|
||||
}
|
||||
|
||||
public static class WsProxy extends ExternalSocks5Proxy {
|
||||
|
||||
public WsLoader.Bean bean;
|
||||
public WsLoader loader;
|
||||
|
||||
public WsProxy(String url) {
|
||||
this(WsLoader.Companion.parse(url));
|
||||
}
|
||||
|
||||
public WsProxy(WsLoader.Bean bean) {
|
||||
this.bean = bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStarted() {
|
||||
return loader != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (loader != null) return;
|
||||
loader = new WsLoader();
|
||||
port = ProxyManager.mkPort();
|
||||
loader.init(bean, port);
|
||||
loader.start();
|
||||
|
||||
if (SharedConfig.proxyEnabled && SharedConfig.currentProxy == this) {
|
||||
ConnectionsManager.setProxySettings(true, address, port, username, password, secret);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (loader == null) return;
|
||||
loader.stop();
|
||||
loader = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAddress() {
|
||||
return bean.getServer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toUrl() {
|
||||
return bean.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRemarks() {
|
||||
return bean.getRemarks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRemarks(String remarks) {
|
||||
bean.setRemarks(remarks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return "WS";
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject toJsonInternal() throws JSONException {
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("type", "ws");
|
||||
obj.put("link", toUrl());
|
||||
return obj;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static LinkedList<ProxyInfo> proxyList = new LinkedList<>();
|
||||
|
||||
public static LinkedList<ProxyInfo> getProxyList() {
|
||||
|
@ -1773,24 +1849,21 @@ public class SharedConfig {
|
|||
|
||||
if (info instanceof ExternalSocks5Proxy) {
|
||||
|
||||
if (info instanceof ExternalSocks5Proxy) {
|
||||
UIUtil.runOnIoDispatcher(() -> {
|
||||
|
||||
UIUtil.runOnIoDispatcher(() -> {
|
||||
try {
|
||||
|
||||
try {
|
||||
((ExternalSocks5Proxy) info).start();
|
||||
|
||||
((ExternalSocks5Proxy) info).start();
|
||||
} catch (Exception e) {
|
||||
|
||||
} catch (Exception e) {
|
||||
FileLog.e(e);
|
||||
AlertUtil.showToast(e);
|
||||
|
||||
FileLog.e(e);
|
||||
AlertUtil.showToast(e);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1867,6 +1940,18 @@ public class SharedConfig {
|
|||
|
||||
}
|
||||
|
||||
} else if (url.startsWith(WS_PROTOCOL) || url.startsWith(WSS_PROTOCOL)) {
|
||||
|
||||
try {
|
||||
|
||||
return new WsProxy(url);
|
||||
|
||||
} catch (Exception ex) {
|
||||
|
||||
throw new InvalidProxyException(ex);
|
||||
|
||||
}
|
||||
|
||||
}/* else if (url.startsWith(RB_PROTOCOL)) {
|
||||
|
||||
try {
|
||||
|
|
|
@ -400,6 +400,8 @@ public class Browser {
|
|||
"vmesss1".equals(uri.getScheme()) ||
|
||||
"ss".equals(uri.getScheme()) ||
|
||||
"ssr".equals(uri.getScheme()) ||
|
||||
"ws".equals(uri.getScheme()) ||
|
||||
"wss".equals(uri.getScheme()) ||
|
||||
"trojan".equals(uri.getScheme())) {
|
||||
return true;
|
||||
} else if ("telegram.dog".equals(host)) {
|
||||
|
|
|
@ -23701,7 +23701,7 @@ public class ChatActivity extends BaseFragment implements NotificationCenter.Not
|
|||
}
|
||||
|
||||
public static boolean isClickableLink(String str) {
|
||||
return str.startsWith("https://") || str.startsWith("vmess://") || str.startsWith("vmess1://") || str.startsWith("ss://") || str.startsWith("ssr://") || str.startsWith("@") || str.startsWith("#") || str.startsWith("$");
|
||||
return str.startsWith("https://") || str.startsWith("vmess://") || str.startsWith("vmess1://") || str.startsWith("ss://") || str.startsWith("ssr://") || str.startsWith("ws://") || str.startsWith("wss://") || str.startsWith("@") || str.startsWith("#") || str.startsWith("$");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -97,6 +97,7 @@ import tw.nekomimi.nekogram.ShadowsocksSettingsActivity;
|
|||
import tw.nekomimi.nekogram.SubSettingsActivity;
|
||||
import tw.nekomimi.nekogram.TrojanSettingsActivity;
|
||||
import tw.nekomimi.nekogram.VmessSettingsActivity;
|
||||
import tw.nekomimi.nekogram.WsSettingsActivity;
|
||||
import tw.nekomimi.nekogram.parts.ProxyChecksKt;
|
||||
import tw.nekomimi.nekogram.sub.SubInfo;
|
||||
import tw.nekomimi.nekogram.sub.SubManager;
|
||||
|
@ -331,6 +332,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
|
|||
private int menu_add_input_vmess = 4;
|
||||
private int menu_add_input_ss = 7;
|
||||
private int menu_add_input_ssr = 8;
|
||||
private int menu_add_input_ws = 9;
|
||||
private int menu_add_input_rb = 17;
|
||||
|
||||
private int menu_add_import_from_clipboard = 5;
|
||||
|
@ -621,6 +623,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
|
|||
|
||||
addItem.addSubItem(menu_add_input_socks, LocaleController.getString("AddProxySocks5", R.string.AddProxySocks5)).setOnClickListener((v) -> presentFragment(new ProxySettingsActivity(0)));
|
||||
addItem.addSubItem(menu_add_input_telegram, LocaleController.getString("AddProxyTelegram", R.string.AddProxyTelegram)).setOnClickListener((v) -> presentFragment(new ProxySettingsActivity(1)));
|
||||
addItem.addSubItem(menu_add_input_ws, LocaleController.getString("AddProxyWs", R.string.AddProxyWs)).setOnClickListener((v) -> presentFragment(new WsSettingsActivity()));
|
||||
|
||||
if (!BuildVars.isMini) {
|
||||
|
||||
|
@ -738,6 +741,8 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
presentFragment(new ShadowsocksRSettingsActivity((SharedConfig.ShadowsocksRProxy) info));
|
||||
}
|
||||
} else if (info instanceof SharedConfig.WsProxy) {
|
||||
presentFragment(new WsSettingsActivity((SharedConfig.WsProxy) info));
|
||||
} else {
|
||||
presentFragment(new ProxySettingsActivity(info));
|
||||
}
|
||||
|
@ -802,6 +807,7 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
|
|||
|
||||
LocaleController.getString("AddProxySocks5", R.string.AddProxySocks5),
|
||||
LocaleController.getString("AddProxyTelegram", R.string.AddProxyTelegram),
|
||||
LocaleController.getString("AddProxyWs", R.string.AddProxyWs),
|
||||
BuildVars.isMini ? null : LocaleController.getString("AddProxyVmess", R.string.AddProxyVmess),
|
||||
BuildVars.isMini ? null : LocaleController.getString("AddProxyTrojan", R.string.AddProxyTrojan),
|
||||
BuildVars.isMini || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ? null : LocaleController.getString("AddProxySS", R.string.AddProxySS),
|
||||
|
@ -821,22 +827,26 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
|
|||
|
||||
} else if (i == 2) {
|
||||
|
||||
presentFragment(new VmessSettingsActivity());
|
||||
presentFragment(new WsSettingsActivity());
|
||||
|
||||
} else if (i == 3) {
|
||||
|
||||
presentFragment(new TrojanSettingsActivity());
|
||||
presentFragment(new VmessSettingsActivity());
|
||||
|
||||
} else if (i == 4) {
|
||||
|
||||
presentFragment(new ShadowsocksSettingsActivity());
|
||||
presentFragment(new TrojanSettingsActivity());
|
||||
|
||||
} else if (i == 5) {
|
||||
|
||||
presentFragment(new ShadowsocksRSettingsActivity());
|
||||
presentFragment(new ShadowsocksSettingsActivity());
|
||||
|
||||
} else if (i == 6) {
|
||||
|
||||
presentFragment(new ShadowsocksRSettingsActivity());
|
||||
|
||||
} else if (i == 7) {
|
||||
|
||||
ProxyUtil.importFromClipboard(getParentActivity());
|
||||
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package tw.nekomimi.nekogram
|
||||
|
||||
import android.graphics.Color
|
||||
import org.telegram.tgnet.ConnectionsManager
|
||||
|
||||
object DataCenter {
|
||||
|
|
73
TMessagesProj/src/main/java/tw/nekomimi/nekogram/WsLoader.kt
Normal file
73
TMessagesProj/src/main/java/tw/nekomimi/nekogram/WsLoader.kt
Normal file
|
@ -0,0 +1,73 @@
|
|||
package tw.nekomimi.nekogram
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import cn.hutool.core.codec.Base64
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import tw.nekomimi.nekogram.tcp2ws.Tcp2wsServer
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
class WsLoader {
|
||||
|
||||
lateinit var server: Tcp2wsServer
|
||||
|
||||
fun init(bean: Bean, port: Int) {
|
||||
server = Tcp2wsServer(bean, port)
|
||||
}
|
||||
|
||||
fun start() {
|
||||
server.start()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (::server.isInitialized) {
|
||||
server.interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun parse(url: String): Bean {
|
||||
val lnk = url.replace("ws://", "http://")
|
||||
.replace("wss://", "https://")
|
||||
.toHttpUrlOrNull() ?: error("Invalid link")
|
||||
val payloadStr = lnk.queryParameter("payload") ?: error("Missing payload")
|
||||
val payload = Base64.decodeStr(payloadStr).split(",")
|
||||
if (payload.size < 5) error("Invalid payload")
|
||||
return Bean(
|
||||
lnk.host,
|
||||
payload,
|
||||
lnk.isHttps,
|
||||
lnk.fragment ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class Bean(
|
||||
var server: String = "",
|
||||
var payload: List<String> = arrayListOf(),
|
||||
var tls: Boolean = true,
|
||||
var remarks: String = ""
|
||||
) {
|
||||
|
||||
var payloadStr: String
|
||||
get() = Base64.encodeUrlSafe(payload.joinToString(","))
|
||||
set(value) {
|
||||
payload = Base64.decodeStr(value).split(",")
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val builder = HttpUrl.Builder()
|
||||
.scheme("http")
|
||||
.host(server)
|
||||
.addQueryParameter("payload", payloadStr);
|
||||
if (remarks.isNotBlank()) {
|
||||
builder.fragment(remarks)
|
||||
}
|
||||
return builder.build().toString().replace("http://", if (tls) "wss://" else "ws://")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,358 @@
|
|||
/*
|
||||
* This is the source code of Telegram for Android v. 5.x.x.
|
||||
* It is licensed under GNU GPL v. 2 or later.
|
||||
* You should have received a copy of the license in this archive (see LICENSE).
|
||||
*
|
||||
* Copyright Nikolai Kudashov, 2013-2018.
|
||||
*/
|
||||
|
||||
package tw.nekomimi.nekogram;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.os.Build;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.telegram.messenger.AndroidUtilities;
|
||||
import org.telegram.messenger.LocaleController;
|
||||
import org.telegram.messenger.R;
|
||||
import org.telegram.messenger.SharedConfig;
|
||||
import org.telegram.ui.ActionBar.ActionBar;
|
||||
import org.telegram.ui.ActionBar.ActionBarMenuItem;
|
||||
import org.telegram.ui.ActionBar.BaseFragment;
|
||||
import org.telegram.ui.ActionBar.Theme;
|
||||
import org.telegram.ui.ActionBar.ThemeDescription;
|
||||
import org.telegram.ui.Cells.TextCheckCell;
|
||||
import org.telegram.ui.Cells.TextInfoPrivacyCell;
|
||||
import org.telegram.ui.Components.EditTextBoldCursor;
|
||||
import org.telegram.ui.Components.LayoutHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
public class WsSettingsActivity extends BaseFragment {
|
||||
|
||||
private EditTextBoldCursor[] inputFields;
|
||||
|
||||
private EditTextBoldCursor ipField;
|
||||
private EditTextBoldCursor payloadField;
|
||||
private TextCheckCell useTlsField;
|
||||
private EditTextBoldCursor remarksField;
|
||||
|
||||
private ScrollView scrollView;
|
||||
private LinearLayout linearLayout2;
|
||||
private LinearLayout inputFieldsContainer;
|
||||
|
||||
private TextInfoPrivacyCell bottomCell;
|
||||
private ActionBarMenuItem doneItem;
|
||||
|
||||
private SharedConfig.WsProxy currentProxyInfo;
|
||||
private WsLoader.Bean currentBean;
|
||||
|
||||
private static final int done_button = 1;
|
||||
|
||||
public class TypeCell extends FrameLayout {
|
||||
|
||||
private TextView textView;
|
||||
private ImageView checkImage;
|
||||
private boolean needDivider;
|
||||
|
||||
public TypeCell(Context context) {
|
||||
super(context);
|
||||
|
||||
setWillNotDraw(false);
|
||||
|
||||
textView = new TextView(context);
|
||||
textView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText));
|
||||
textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
textView.setLines(1);
|
||||
textView.setMaxLines(1);
|
||||
textView.setSingleLine(true);
|
||||
textView.setEllipsize(TextUtils.TruncateAt.END);
|
||||
textView.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL);
|
||||
addView(textView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, (LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.TOP, LocaleController.isRTL ? 23 + 48 : 21, 0, LocaleController.isRTL ? 21 : 23, 0));
|
||||
|
||||
checkImage = new ImageView(context);
|
||||
checkImage.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_featuredStickers_addedIcon), PorterDuff.Mode.SRC_IN));
|
||||
checkImage.setImageResource(R.drawable.sticker_added);
|
||||
addView(checkImage, LayoutHelper.createFrame(19, 14, (LocaleController.isRTL ? Gravity.LEFT : Gravity.RIGHT) | Gravity.CENTER_VERTICAL, 21, 0, 21, 0));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(50) + (needDivider ? 1 : 0), MeasureSpec.EXACTLY));
|
||||
}
|
||||
|
||||
public void setValue(String name, boolean checked, boolean divider) {
|
||||
textView.setText(name);
|
||||
checkImage.setVisibility(checked ? VISIBLE : INVISIBLE);
|
||||
needDivider = divider;
|
||||
}
|
||||
|
||||
public void setTypeChecked(boolean value) {
|
||||
checkImage.setVisibility(value ? VISIBLE : INVISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (needDivider) {
|
||||
canvas.drawLine(0, getMeasuredHeight() - 1, getMeasuredWidth(), getMeasuredHeight() - 1, Theme.dividerPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public WsSettingsActivity() {
|
||||
super();
|
||||
currentBean = new WsLoader.Bean();
|
||||
}
|
||||
|
||||
public WsSettingsActivity(SharedConfig.WsProxy proxyInfo) {
|
||||
super();
|
||||
currentProxyInfo = proxyInfo;
|
||||
currentBean = proxyInfo.bean;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
AndroidUtilities.requestAdjustResize(getParentActivity(), classGuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View createView(Context context) {
|
||||
actionBar.setTitle(LocaleController.getString("ProxyDetails", R.string.ProxyDetails));
|
||||
actionBar.setBackButtonImage(R.drawable.ic_ab_back);
|
||||
actionBar.setAllowOverlayTitle(false);
|
||||
if (AndroidUtilities.isTablet()) {
|
||||
actionBar.setOccupyStatusBar(false);
|
||||
}
|
||||
|
||||
actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() {
|
||||
@Override
|
||||
public void onItemClick(int id) {
|
||||
if (id == -1) {
|
||||
finishFragment();
|
||||
} else if (id == done_button) {
|
||||
|
||||
if (getParentActivity() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (StrUtil.isBlank(ipField.getText())) {
|
||||
|
||||
ipField.requestFocus();
|
||||
AndroidUtilities.showKeyboard(ipField);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
if (StrUtil.isBlank(payloadField.getText())) {
|
||||
|
||||
payloadField.requestFocus();
|
||||
AndroidUtilities.showKeyboard(payloadField);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
currentBean.setServer(ipField.getText().toString());
|
||||
currentBean.setPayloadStr(payloadField.getText().toString());
|
||||
currentBean.setTls(useTlsField.isChecked());
|
||||
currentBean.setRemarks(remarksField.getText().toString());
|
||||
|
||||
if (currentProxyInfo == null) {
|
||||
currentProxyInfo = new SharedConfig.WsProxy(currentBean);
|
||||
SharedConfig.addProxy(currentProxyInfo);
|
||||
SharedConfig.setCurrentProxy(currentProxyInfo);
|
||||
} else {
|
||||
currentProxyInfo.proxyCheckPingId = 0;
|
||||
currentProxyInfo.availableCheckTime = 0;
|
||||
currentProxyInfo.ping = 0;
|
||||
SharedConfig.saveProxyList();
|
||||
SharedConfig.setProxyEnable(false);
|
||||
}
|
||||
|
||||
finishFragment();
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
doneItem = actionBar.createMenu().addItemWithWidth(done_button, R.drawable.ic_done, AndroidUtilities.dp(56));
|
||||
doneItem.setContentDescription(LocaleController.getString("Done", R.string.Done));
|
||||
|
||||
fragmentView = new FrameLayout(context);
|
||||
FrameLayout frameLayout = (FrameLayout) fragmentView;
|
||||
fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray));
|
||||
|
||||
scrollView = new ScrollView(context);
|
||||
scrollView.setFillViewport(true);
|
||||
AndroidUtilities.setScrollViewEdgeEffectColor(scrollView, Theme.getColor(Theme.key_actionBarDefault));
|
||||
frameLayout.addView(scrollView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT));
|
||||
|
||||
linearLayout2 = new LinearLayout(context);
|
||||
linearLayout2.setOrientation(LinearLayout.VERTICAL);
|
||||
scrollView.addView(linearLayout2, new ScrollView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
inputFieldsContainer = new LinearLayout(context);
|
||||
inputFieldsContainer.setOrientation(LinearLayout.VERTICAL);
|
||||
inputFieldsContainer.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// bring to front for transitions
|
||||
inputFieldsContainer.setElevation(AndroidUtilities.dp(1f));
|
||||
inputFieldsContainer.setOutlineProvider(null);
|
||||
}
|
||||
linearLayout2.addView(inputFieldsContainer, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT));
|
||||
|
||||
inputFields = new EditTextBoldCursor[5];
|
||||
|
||||
for (int a = 0; a < 5; a++) {
|
||||
FrameLayout container = new FrameLayout(context);
|
||||
EditTextBoldCursor cursor = mkCursor();
|
||||
inputFields[a] = cursor;
|
||||
cursor.setImeOptions(EditorInfo.IME_ACTION_NEXT | EditorInfo.IME_FLAG_NO_EXTRACT_UI);
|
||||
switch (a) {
|
||||
case 0:
|
||||
ipField = cursor;
|
||||
cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
cursor.setHintText(LocaleController.getString("UseProxyAddress", R.string.UseProxyAddress));
|
||||
cursor.setText(currentBean.getServer());
|
||||
break;
|
||||
case 1:
|
||||
payloadField = cursor;
|
||||
cursor.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
cursor.setHintText(LocaleController.getString("WsPayload", R.string.WsPayload));
|
||||
cursor.setText(currentBean.getPayloadStr());
|
||||
break;
|
||||
case 2:
|
||||
remarksField = cursor;
|
||||
cursor.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
cursor.setHintText(LocaleController.getString("ProxyRemarks", R.string.ProxyRemarks));
|
||||
cursor.setText(currentBean.getRemarks());
|
||||
break;
|
||||
}
|
||||
cursor.setSelection(cursor.length());
|
||||
|
||||
cursor.setPadding(0, 0, 0, 0);
|
||||
container.addView(cursor, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 17, a == 0 ? 12 : 0, 17, 0));
|
||||
|
||||
}
|
||||
|
||||
inputFieldsContainer.addView((View) ipField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64));
|
||||
inputFieldsContainer.addView((View) payloadField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64));
|
||||
|
||||
FrameLayout container = new FrameLayout(context);
|
||||
useTlsField = new TextCheckCell(context);
|
||||
useTlsField.setBackground(Theme.getSelectorDrawable(false));
|
||||
useTlsField.setTextAndCheck(LocaleController.getString("VmessTls", R.string.VmessTls), currentBean.getTls(), false);
|
||||
container.addView(useTlsField, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.LEFT | Gravity.TOP, 0, 0, 0, 0));
|
||||
|
||||
useTlsField.setOnClickListener((v) -> useTlsField.setChecked(!useTlsField.isChecked()));
|
||||
|
||||
inputFieldsContainer.addView(container, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64));
|
||||
|
||||
inputFieldsContainer.addView((View) remarksField.getParent(), LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 64));
|
||||
|
||||
bottomCell = new TextInfoPrivacyCell(context);
|
||||
bottomCell.setBackground(Theme.getThemedDrawable(context, R.drawable.greydivider_bottom, Theme.key_windowBackgroundGrayShadow));
|
||||
bottomCell.setText(LocaleController.getString("ProxyInfoWS", R.string.ProxyInfoWS));
|
||||
linearLayout2.addView(bottomCell, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT));
|
||||
|
||||
return fragmentView;
|
||||
|
||||
}
|
||||
|
||||
EditTextBoldCursor mkCursor() {
|
||||
|
||||
EditTextBoldCursor cursor = new EditTextBoldCursor(getParentActivity());
|
||||
cursor.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
cursor.setHintColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText));
|
||||
cursor.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText));
|
||||
cursor.setBackground(null);
|
||||
cursor.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText));
|
||||
cursor.setCursorSize(AndroidUtilities.dp(20));
|
||||
cursor.setCursorWidth(1.5f);
|
||||
cursor.setSingleLine(true);
|
||||
cursor.setGravity((LocaleController.isRTL ? Gravity.RIGHT : Gravity.LEFT) | Gravity.CENTER_VERTICAL);
|
||||
cursor.setHeaderHintColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueHeader));
|
||||
cursor.setTransformHintToHeader(true);
|
||||
cursor.setLineColors(Theme.getColor(Theme.key_windowBackgroundWhiteInputField), Theme.getColor(Theme.key_windowBackgroundWhiteInputFieldActivated), Theme.getColor(Theme.key_windowBackgroundWhiteRedText3));
|
||||
return cursor;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransitionAnimationEnd(boolean isOpen, boolean backward) {
|
||||
if (isOpen && !backward && currentProxyInfo == null) {
|
||||
ipField.requestFocus();
|
||||
AndroidUtilities.showKeyboard(ipField);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<ThemeDescription> getThemeDescriptions() {
|
||||
final ThemeDescription.ThemeDescriptionDelegate delegate = () -> {
|
||||
if (inputFields != null) {
|
||||
for (int i = 0; i < inputFields.length; i++) {
|
||||
inputFields[i].setLineColors(Theme.getColor(Theme.key_windowBackgroundWhiteInputField),
|
||||
Theme.getColor(Theme.key_windowBackgroundWhiteInputFieldActivated),
|
||||
Theme.getColor(Theme.key_windowBackgroundWhiteRedText3));
|
||||
}
|
||||
}
|
||||
};
|
||||
ArrayList<ThemeDescription> arrayList = new ArrayList<>();
|
||||
arrayList.add(new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundGray));
|
||||
arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_actionBarDefault));
|
||||
arrayList.add(new ThemeDescription(scrollView, ThemeDescription.FLAG_LISTGLOWCOLOR, null, null, null, null, Theme.key_actionBarDefault));
|
||||
arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_actionBarDefaultIcon));
|
||||
arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_TITLECOLOR, null, null, null, null, Theme.key_actionBarDefaultTitle));
|
||||
arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarDefaultSelector));
|
||||
arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCH, null, null, null, null, Theme.key_actionBarDefaultSearch));
|
||||
arrayList.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SEARCHPLACEHOLDER, null, null, null, null, Theme.key_actionBarDefaultSearchPlaceholder));
|
||||
arrayList.add(new ThemeDescription(inputFieldsContainer, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite));
|
||||
arrayList.add(new ThemeDescription(linearLayout2, 0, new Class[]{View.class}, Theme.dividerPaint, null, null, Theme.key_divider));
|
||||
|
||||
arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteBlueText4));
|
||||
arrayList.add(new ThemeDescription(null, 0, null, null, null, null, delegate, Theme.key_windowBackgroundWhiteGrayText2));
|
||||
|
||||
|
||||
if (inputFields != null) {
|
||||
for (int a = 0; a < inputFields.length; a++) {
|
||||
arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText));
|
||||
arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText));
|
||||
arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_HINTTEXTCOLOR | ThemeDescription.FLAG_PROGRESSBAR, null, null, null, null, Theme.key_windowBackgroundWhiteBlueHeader));
|
||||
arrayList.add(new ThemeDescription(inputFields[a], ThemeDescription.FLAG_CURSORCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText));
|
||||
arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteInputField));
|
||||
arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteInputFieldActivated));
|
||||
arrayList.add(new ThemeDescription(null, 0, null, null, null, delegate, Theme.key_windowBackgroundWhiteRedText3));
|
||||
}
|
||||
} else {
|
||||
arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_TEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText));
|
||||
arrayList.add(new ThemeDescription(null, ThemeDescription.FLAG_HINTTEXTCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteHintText));
|
||||
}
|
||||
|
||||
arrayList.add(new ThemeDescription(bottomCell, ThemeDescription.FLAG_BACKGROUNDFILTER, new Class[]{TextInfoPrivacyCell.class}, null, null, null, Theme.key_windowBackgroundGrayShadow));
|
||||
arrayList.add(new ThemeDescription(bottomCell, 0, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteGrayText4));
|
||||
arrayList.add(new ThemeDescription(bottomCell, ThemeDescription.FLAG_LINKCOLOR, new Class[]{TextInfoPrivacyCell.class}, new String[]{"textView"}, null, null, null, Theme.key_windowBackgroundWhiteLinkText));
|
||||
|
||||
return arrayList;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
package tw.nekomimi.nekogram.tcp2ws;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
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 cn.hutool.core.util.StrUtil;
|
||||
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.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 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);
|
||||
if (target == null) {
|
||||
target = mapper.get(StrUtil.subBefore(server, ".", true) + ".");
|
||||
}
|
||||
if (target == null) {
|
||||
FileLog.e("No route for ip " + server);
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (bean.getPayload().size() >= target) {
|
||||
server = bean.getPayload().get(target - 1);
|
||||
}
|
||||
|
||||
FileLog.d("Connect to DC" + target + ": " + (bean.getTls() ? "wss://" : "ws://") + server + "." + bean.getServer() + "/api");
|
||||
|
||||
new 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) {
|
||||
ProxyHandler.this.sendToClient(bytes.toByteArray());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,309 @@
|
|||
/*
|
||||
package tw.nekomimi.nekogram.tcp2ws;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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 static java.lang.String.format;
|
||||
import static org.bbottema.javasocksproxyserver.Utils.getSocketInfo;
|
||||
|
||||
public class ProxyHandler implements Runnable {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ProxyHandler.class);
|
||||
|
||||
private InputStream m_ClientInput = null;
|
||||
private OutputStream m_ClientOutput = null;
|
||||
private InputStream m_ServerInput = null;
|
||||
private OutputStream m_ServerOutput = null;
|
||||
private Object m_lock;
|
||||
private Socks4Impl comm = null;
|
||||
|
||||
Socket m_ClientSocket;
|
||||
Socket m_ServerSocket = null;
|
||||
byte[] m_Buffer = new byte[SocksConstants.DEFAULT_BUF_SIZE];
|
||||
|
||||
public ProxyHandler(Socket clientSocket) {
|
||||
m_lock = this;
|
||||
m_ClientSocket = clientSocket;
|
||||
try {
|
||||
m_ClientSocket.setSoTimeout(SocksConstants.DEFAULT_PROXY_TIMEOUT);
|
||||
} catch (SocketException e) {
|
||||
LOGGER.error("Socket Exception during seting Timeout.");
|
||||
}
|
||||
LOGGER.debug("Proxy Created.");
|
||||
}
|
||||
|
||||
public void setLock(Object lock) {
|
||||
this.m_lock = lock;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
LOGGER.debug("Proxy Started.");
|
||||
setLock(this);
|
||||
|
||||
if (prepareClient()) {
|
||||
processRelay();
|
||||
close();
|
||||
} else {
|
||||
LOGGER.error("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_ServerOutput != null) {
|
||||
m_ServerOutput.flush();
|
||||
m_ServerOutput.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();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
m_ServerSocket = null;
|
||||
m_ClientSocket = null;
|
||||
|
||||
LOGGER.debug("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) {
|
||||
LOGGER.error("Sending data to client");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void sendToServer(byte[] buffer, int len) {
|
||||
if (m_ServerOutput != null && len > 0 && len <= buffer.length) {
|
||||
try {
|
||||
m_ServerOutput.write(buffer, 0, len);
|
||||
m_ServerOutput.flush();
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Sending data to server");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return m_ClientSocket != null && m_ServerSocket != null;
|
||||
}
|
||||
|
||||
public void connectToServer(String server, int port) throws IOException {
|
||||
|
||||
if (server.equals("")) {
|
||||
close();
|
||||
LOGGER.error("Invalid Remote Host Name - Empty String !!!");
|
||||
return;
|
||||
}
|
||||
|
||||
m_ServerSocket = new Socket(server, port);
|
||||
m_ServerSocket.setSoTimeout(SocksConstants.DEFAULT_PROXY_TIMEOUT);
|
||||
|
||||
LOGGER.debug("Connected to " + getSocketInfo(m_ServerSocket));
|
||||
prepareServer();
|
||||
}
|
||||
|
||||
protected void prepareServer() throws IOException {
|
||||
synchronized (m_lock) {
|
||||
m_ServerInput = m_ServerSocket.getInputStream();
|
||||
m_ServerOutput = m_ServerSocket.getOutputStream();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
LOGGER.error("Proxy - can't get I/O streams!");
|
||||
LOGGER.error(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:
|
||||
LOGGER.error("Invalid SOKCS version : " + SOCKS_Version);
|
||||
return;
|
||||
}
|
||||
LOGGER.debug("Accepted SOCKS " + SOCKS_Version + " Request.");
|
||||
|
||||
comm.authenticate(SOCKS_Version);
|
||||
comm.getClientCommand();
|
||||
|
||||
switch (comm.socksCommand) {
|
||||
case SocksConstants.SC_CONNECT:
|
||||
comm.connect();
|
||||
relay();
|
||||
break;
|
||||
|
||||
case SocksConstants.SC_BIND:
|
||||
comm.bind();
|
||||
relay();
|
||||
break;
|
||||
|
||||
case SocksConstants.SC_UDP:
|
||||
comm.udp();
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(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() {
|
||||
boolean isActive = true;
|
||||
|
||||
while (isActive) {
|
||||
|
||||
//---> Check for client data <---
|
||||
|
||||
int dlen = checkClientData();
|
||||
|
||||
if (dlen < 0) {
|
||||
isActive = false;
|
||||
}
|
||||
if (dlen > 0) {
|
||||
logData(dlen, "Cli data");
|
||||
sendToServer(m_Buffer, dlen);
|
||||
}
|
||||
|
||||
//---> Check for Server data <---
|
||||
dlen = checkServerData();
|
||||
|
||||
if (dlen < 0) isActive = false;
|
||||
if (dlen > 0) {
|
||||
logData(dlen, "Srv data");
|
||||
sendToClient(m_Buffer, dlen);
|
||||
}
|
||||
|
||||
Thread.yield();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
LOGGER.debug("Client connection Closed!");
|
||||
close(); // Close the server on this exception
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (dlen < 0) close();
|
||||
|
||||
return dlen;
|
||||
}
|
||||
}
|
||||
|
||||
public int checkServerData() {
|
||||
synchronized (m_lock) {
|
||||
// The client side is not opened.
|
||||
if (m_ServerInput == null) return -1;
|
||||
|
||||
int dlen;
|
||||
|
||||
try {
|
||||
dlen = m_ServerInput.read(m_Buffer, 0, SocksConstants.DEFAULT_BUF_SIZE);
|
||||
} catch (InterruptedIOException e) {
|
||||
return 0;
|
||||
} catch (IOException e) {
|
||||
LOGGER.debug("Server connection Closed!");
|
||||
close(); // Close the server on this exception
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (dlen < 0) {
|
||||
close();
|
||||
}
|
||||
|
||||
return dlen;
|
||||
}
|
||||
}
|
||||
|
||||
private void logData(final int traffic, final String dataSource) {
|
||||
LOGGER.debug(format("%s : %s >> <%s/%s:%d> : %d bytes.",
|
||||
dataSource,
|
||||
getSocketInfo(m_ClientSocket),
|
||||
comm.m_ServerIP.getHostName(),
|
||||
comm.m_ServerIP.getHostAddress(),
|
||||
comm.m_nServerPort, traffic));
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return m_ServerSocket.getPort();
|
||||
}
|
||||
}*/
|
|
@ -0,0 +1,162 @@
|
|||
package tw.nekomimi.nekogram.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()));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
package tw.nekomimi.nekogram.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
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package tw.nekomimi.nekogram.tcp2ws;
|
||||
|
||||
public interface SocksConstants {
|
||||
|
||||
// refactor
|
||||
int LISTEN_TIMEOUT = 200;
|
||||
int DEFAULT_SERVER_TIMEOUT = 200;
|
||||
|
||||
int DEFAULT_BUF_SIZE = 4096;
|
||||
int DEFAULT_PROXY_TIMEOUT = 10;
|
||||
|
||||
byte SOCKS5_Version = 0x05;
|
||||
byte SOCKS4_Version = 0x04;
|
||||
|
||||
byte SC_CONNECT = 0x01;
|
||||
byte SC_BIND = 0x02;
|
||||
byte SC_UDP = 0x03;
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package tw.nekomimi.nekogram.tcp2ws;
|
||||
|
||||
import org.telegram.messenger.FileLog;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.HashMap;
|
||||
|
||||
import tw.nekomimi.nekogram.WsLoader;
|
||||
|
||||
public class Tcp2wsServer extends Thread {
|
||||
|
||||
public WsLoader.Bean bean;
|
||||
public 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.50", 1);
|
||||
mapper.put("149.154.175.53", 1);
|
||||
mapper.put("149.154.175.55", 1);
|
||||
mapper.put("149.154.167.51", 2);
|
||||
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.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.175.5", 1);
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
FileLog.d("SOCKS server started...");
|
||||
try {
|
||||
handleClients(port);
|
||||
FileLog.d("SOCKS server stopped...");
|
||||
} catch (IOException e) {
|
||||
FileLog.d("SOCKS server crashed...");
|
||||
interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleClients(int port) throws IOException {
|
||||
final ServerSocket listenSocket = new ServerSocket(port);
|
||||
listenSocket.setSoTimeout(SocksConstants.LISTEN_TIMEOUT);
|
||||
Tcp2wsServer.this.port = listenSocket.getLocalPort();
|
||||
FileLog.d("SOCKS server listening at port: " + listenSocket.getLocalPort());
|
||||
|
||||
while (isAlive() && !isInterrupted()) {
|
||||
handleNextClient(listenSocket);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package tw.nekomimi.nekogram.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());
|
||||
}
|
||||
}
|
|
@ -34,6 +34,8 @@ import com.v2ray.ang.V2RayConfig.SS_PROTOCOL
|
|||
import com.v2ray.ang.V2RayConfig.TROJAN_PROTOCOL
|
||||
import com.v2ray.ang.V2RayConfig.VMESS1_PROTOCOL
|
||||
import com.v2ray.ang.V2RayConfig.VMESS_PROTOCOL
|
||||
import com.v2ray.ang.V2RayConfig.WSS_PROTOCOL
|
||||
import com.v2ray.ang.V2RayConfig.WS_PROTOCOL
|
||||
import com.v2ray.ang.dto.AngConfig
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.json.JSONArray
|
||||
|
@ -202,6 +204,8 @@ object ProxyUtil {
|
|||
line.startsWith(VMESS1_PROTOCOL) ||
|
||||
line.startsWith(SS_PROTOCOL) ||
|
||||
line.startsWith(SSR_PROTOCOL) ||
|
||||
line.startsWith(WS_PROTOCOL) ||
|
||||
line.startsWith(WSS_PROTOCOL) ||
|
||||
line.startsWith(TROJAN_PROTOCOL) /*||
|
||||
line.startsWith(RB_PROTOCOL)*/) {
|
||||
|
||||
|
@ -240,6 +244,8 @@ object ProxyUtil {
|
|||
line.startsWith(VMESS1_PROTOCOL) ||
|
||||
line.startsWith(SS_PROTOCOL) ||
|
||||
line.startsWith(SSR_PROTOCOL) ||
|
||||
line.startsWith(WS_PROTOCOL) ||
|
||||
line.startsWith(WSS_PROTOCOL) ||
|
||||
line.startsWith(TROJAN_PROTOCOL) /*||
|
||||
line.startsWith(RB_PROTOCOL)*/) {
|
||||
|
||||
|
@ -273,7 +279,10 @@ object ProxyUtil {
|
|||
line.startsWith(VMESS1_PROTOCOL) ||
|
||||
line.startsWith(SS_PROTOCOL) ||
|
||||
line.startsWith(SSR_PROTOCOL) ||
|
||||
line.startsWith(WS_PROTOCOL) ||
|
||||
line.startsWith(WSS_PROTOCOL) ||
|
||||
line.startsWith(TROJAN_PROTOCOL) /*||
|
||||
|
||||
line.startsWith(RB_PROTOCOL)*/) {
|
||||
|
||||
runCatching { proxies.add(SharedConfig.parseProxyInfo(line)) }.onFailure {
|
||||
|
@ -339,7 +348,11 @@ object ProxyUtil {
|
|||
|
||||
} else if (link.startsWith(SSR_PROTOCOL)) {
|
||||
|
||||
AndroidUtilities.showShadowsocksRAlert(ctx, SharedConfig.ShadowsocksRProxy(link))
|
||||
AndroidUtilities.showWsAlert(ctx, SharedConfig.WsProxy(link))
|
||||
|
||||
} else if (link.startsWith(WS_PROTOCOL) || link.startsWith(WSS_PROTOCOL)) {
|
||||
|
||||
AndroidUtilities.showWsAlert(ctx, SharedConfig.WsProxy(link))
|
||||
|
||||
} else {
|
||||
|
||||
|
@ -407,6 +420,16 @@ object ProxyUtil {
|
|||
|
||||
SharedConfig.ShadowsocksRProxy(link)
|
||||
|
||||
} else if (link.startsWith(WS_PROTOCOL) || link.startsWith(WSS_PROTOCOL)) {
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
|
||||
error(LocaleController.getString("MinApi21Required", R.string.MinApi21Required))
|
||||
|
||||
}
|
||||
|
||||
SharedConfig.WsProxy(link)
|
||||
|
||||
} else {
|
||||
|
||||
SharedConfig.ProxyInfo.fromUrl(link)
|
||||
|
|
|
@ -228,4 +228,7 @@
|
|||
<string name="UsePersiancalendar">Use persian calendar</string>
|
||||
<string name="UsePersiancalendarInfo">Display date with Solar Hijri calendar</string>
|
||||
<string name="DisplayPersianCalendarByLatin">Display persian calendar by latin characters</string>
|
||||
<string name="AddProxyWs">Add WebSocket Relay</string>
|
||||
<string name="ProxyInfoWS">WebSocket Relay Settings</string>
|
||||
<string name="WsPayload">Payload</string>
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user