1
0
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:
世界 2021-04-11 00:22:59 +08:00
parent 1623657bba
commit 95c574f012
No known key found for this signature in database
GPG Key ID: CD109927C34A63C4
21 changed files with 1844 additions and 28 deletions

View File

@ -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"

View File

@ -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>

View File

@ -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"

View File

@ -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 {

View File

@ -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\\$\\-\\_"

View File

@ -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 {

View File

@ -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)) {

View File

@ -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

View File

@ -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 {

View File

@ -1,5 +1,6 @@
package tw.nekomimi.nekogram
import android.graphics.Color
import org.telegram.tgnet.ConnectionsManager
object DataCenter {

View 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://")
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}*/

View File

@ -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()));
}
}

View File

@ -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
}
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}

View File

@ -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)

View File

@ -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>