From 78f63c6aa08bab9543487592202df194482f3798 Mon Sep 17 00:00:00 2001 From: dovecoteescapee Date: Sat, 10 Aug 2024 15:41:32 +0300 Subject: [PATCH] Add command line style settings and refactor settings --- app/src/main/cpp/main.h | 6 + app/src/main/cpp/native-lib.c | 60 ++- app/src/main/cpp/utils.c | 418 +++++++++++++++++- app/src/main/cpp/utils.h | 1 + .../byedpi/activities/MainActivity.kt | 25 +- .../byedpi/activities/SettingsActivity.kt | 11 +- .../byedpi/core/ByeDpiProxy.kt | 29 +- .../byedpi/core/ByeDpiProxyPreferences.kt | 49 +- .../ByeDpiCommandLineSettingsFragment.kt | 11 + .../fragments/ByeDpiUISettingsFragment.kt | 146 ++++++ .../byedpi/fragments/MainSettingsFragment.kt | 92 ++++ .../byedpi/fragments/SettingsFragment.kt | 131 ------ .../byedpi/services/ByeDpiProxyService.kt | 17 +- .../byedpi/services/ByeDpiVpnService.kt | 21 +- .../byedpi/services/LifecycleVpnService.kt | 6 +- .../byedpi/services/QuickTileService.kt | 12 +- .../byedpi/services/ServiceManager.kt | 2 +- .../byedpi/utility/ArgumentsUtils.kt | 41 ++ .../byedpi/utility/PreferencesUtils.kt | 12 +- .../byedpi/utility/ValidateUtils.kt | 65 +++ app/src/main/res/values/strings.xml | 10 + app/src/main/res/xml/byedpi_cmd_settings.xml | 25 ++ .../{settings.xml => byedpi_ui_settings.xml} | 106 ++--- app/src/main/res/xml/main_settings.xml | 76 ++++ 24 files changed, 1090 insertions(+), 282 deletions(-) create mode 100644 app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/ByeDpiCommandLineSettingsFragment.kt create mode 100644 app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/ByeDpiUISettingsFragment.kt create mode 100644 app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/MainSettingsFragment.kt delete mode 100644 app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/SettingsFragment.kt create mode 100644 app/src/main/java/io/github/dovecoteescapee/byedpi/utility/ArgumentsUtils.kt create mode 100644 app/src/main/java/io/github/dovecoteescapee/byedpi/utility/ValidateUtils.kt create mode 100644 app/src/main/res/xml/byedpi_cmd_settings.xml rename app/src/main/res/xml/{settings.xml => byedpi_ui_settings.xml} (67%) create mode 100644 app/src/main/res/xml/main_settings.xml diff --git a/app/src/main/cpp/main.h b/app/src/main/cpp/main.h index 13297db..f6f8f34 100644 --- a/app/src/main/cpp/main.h +++ b/app/src/main/cpp/main.h @@ -10,3 +10,9 @@ int get_addr(const char *str, struct sockaddr_ina *addr); void *add(void **root, int *n, size_t ss); void clear_params(void); + +char *ftob(const char *str, ssize_t *sl); + +struct mphdr *parse_hosts(char *buffer, size_t size); + +int parse_offset(struct part *part, const char *str); diff --git a/app/src/main/cpp/native-lib.c b/app/src/main/cpp/native-lib.c index 6a2ac39..19a60af 100644 --- a/app/src/main/cpp/native-lib.c +++ b/app/src/main/cpp/native-lib.c @@ -19,12 +19,43 @@ const enum demode DESYNC_METHODS[] = { DESYNC_OOB, }; -JNIEXPORT jint JNI_OnLoad(JavaVM *vm, __attribute__((unused)) void *reserved) { - oob_data.data = NULL; +JNIEXPORT jint JNI_OnLoad( + __attribute__((unused)) JavaVM *vm, + __attribute__((unused)) void *reserved) { default_params = params; return JNI_VERSION_1_6; } +JNIEXPORT jint JNICALL +Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniCreateSocketWithCommandLine( + JNIEnv *env, + __attribute__((unused)) jobject thiz, + jobjectArray args) { + int argc = (*env)->GetArrayLength(env, args); + char *argv[argc]; + for (int i = 0; i < argc; i++) { + jstring arg = (jstring) (*env)->GetObjectArrayElement(env, args, i); + const char *arg_str = (*env)->GetStringUTFChars(env, arg, 0); + argv[i] = strdup(arg_str); + (*env)->ReleaseStringUTFChars(env, arg, arg_str); + } + + int res = parse_args(argc, argv); + if (res < 0) { + uniperror("parse_args"); + return -1; + } + + int fd = listen_socket((struct sockaddr_ina *)¶ms.laddr); + if (fd < 0) { + uniperror("listen_socket"); + return -1; + } + LOG(LOG_S, "listen_socket, fd: %d", fd); + + return fd; +} + JNIEXPORT jint JNICALL Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniCreateSocket( JNIEnv *env, @@ -36,6 +67,9 @@ Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniCreateSocket( jint default_ttl, jboolean custom_ttl, jboolean no_domain, + jboolean desync_http, + jboolean desync_https, + jboolean desync_udp, jint desync_method, jint split_position, jboolean split_at_host, @@ -90,6 +124,10 @@ Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniCreateSocket( } dp->ttl = fake_ttl; + dp->proto = + IS_HTTP * desync_http | + IS_HTTPS * desync_https | + IS_UDP * desync_udp; dp->mod_http = MH_HMIX * host_mixed_case | MH_DMIX * domain_mixed_case | @@ -108,7 +146,9 @@ Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniCreateSocket( enum demode mode = DESYNC_METHODS[desync_method]; - part->flag = split_at_host ? OFFSET_SNI : 0; + int offset_flag = dp->proto || desync_https ? OFFSET_SNI : OFFSET_HOST; + + part->flag = split_at_host ? offset_flag : 0; part->pos = split_position; part->m = mode; @@ -125,7 +165,7 @@ Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniCreateSocket( return -1; } - tlsrec_part->flag = tls_record_split_at_sni ? OFFSET_SNI : 0; + tlsrec_part->flag = tls_record_split_at_sni ? offset_flag : 0; tlsrec_part->pos = tls_record_split_position; } @@ -154,6 +194,16 @@ Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniCreateSocket( (*env)->ReleaseStringUTFChars(env, custom_oob_data, oob); } + if (dp->proto) { + dp = add((void *)¶ms.dp, + ¶ms.dp_count, sizeof(struct desync_params)); + if (!dp) { + uniperror("add"); + clear_params(); + return -1; + } + } + params.mempool = mem_pool(0); if (!params.mempool) { uniperror("mem_pool"); @@ -200,4 +250,4 @@ Java_io_github_dovecoteescapee_byedpi_core_ByeDpiProxy_jniStopProxy( return get_e(); } return 0; -} +} \ No newline at end of file diff --git a/app/src/main/cpp/utils.c b/app/src/main/cpp/utils.c index f4c902f..dff4d36 100644 --- a/app/src/main/cpp/utils.c +++ b/app/src/main/cpp/utils.c @@ -1,6 +1,12 @@ -#include "utils.h" +#include +#include +#include + #include "byedpi/params.h" +#include "error.h" #include "main.h" +#include "packets.h" +#include "utils.h" struct params default_params; @@ -8,3 +14,413 @@ void reset_params(void) { clear_params(); params = default_params; } + +extern const struct option options[35]; + +int parse_args(int argc, char **argv) { + int optc = sizeof(options)/sizeof(*options); + for (int i = 0, e = optc; i < e; i++) + optc += options[i].has_arg; + + char opt[optc + 1]; + opt[optc] = 0; + + for (int i = 0, o = 0; o < optc; i++, o++) { + opt[o] = options[i].val; + for (int c = options[i].has_arg; c; c--) { + o++; + opt[o] = ':'; + } + } + + params.laddr.sin6_port = htons(1080); + + int rez; + int invalid = 0; + + long val; + char *end = 0; + + struct desync_params *dp = add((void *)¶ms.dp, + ¶ms.dp_count, sizeof(struct desync_params)); + if (!dp) { + reset_params(); + return -1; + } + + optind = optreset = 1; + + while (!invalid && (rez = getopt_long_only( + argc, argv, opt, options, 0)) != -1) { + switch (rez) { + + case 'N': + params.resolve = 0; + break; + case 'X': + params.ipv6 = 0; + break; + case 'U': + params.udp = 0; + break; + + case 'i': + if (get_addr(optarg, + (struct sockaddr_ina *)¶ms.laddr) < 0) + invalid = 1; + break; + + case 'p': + val = strtol(optarg, &end, 0); + if (val <= 0 || val > 0xffff || *end) + invalid = 1; + else + params.laddr.sin6_port = htons(val); + break; + + case 'I': + if (get_addr(optarg, + (struct sockaddr_ina *)¶ms.baddr) < 0) + invalid = 1; + break; + + case 'b': + val = strtol(optarg, &end, 0); + if (val <= 0 || val > INT_MAX/4 || *end) + invalid = 1; + else + params.bfsize = val; + break; + + case 'c': + val = strtol(optarg, &end, 0); + if (val <= 0 || val >= (0xffff/2) || *end) + invalid = 1; + else + params.max_open = val; + break; + + case 'x': // + params.debug = strtol(optarg, 0, 0); + if (params.debug < 0) + invalid = 1; + break; + + // desync options + + case 'F': + params.tfo = 1; + break; + + case 'A': + dp = add((void *)¶ms.dp, ¶ms.dp_count, + sizeof(struct desync_params)); + if (!dp) { + reset_params(); + return -1; + } + if (!optarg) { + dp->detect |= DETECT_TORST; + break; + } + end = optarg; + while (end && !invalid) { + switch (*end) { + case 't': + dp->detect |= DETECT_TORST; + break; + case 'r': + dp->detect |= DETECT_HTTP_LOCAT; + break; + case 'c': + dp->detect |= DETECT_HTTP_CLERR; + break; + case 's': + dp->detect |= DETECT_TLS_INVSID; + break; + case 'a': + dp->detect |= DETECT_TLS_ALERT; + break; + case 'n': + break; + default: + invalid = 1; + continue; + } + end = strchr(end, ','); + if (end) end++; + } + break; + + case 'u': + val = strtol(optarg, &end, 0); + if (val <= 0 || *end) + invalid = 1; + else + params.cache_ttl = val; + break; + + case 'T':; + float f = strtof(optarg, &end); + val = (long)(f * 1000); + if (val <= 0 || val > UINT_MAX || *end) + invalid = 1; + else + params.timeout = val; + break; + + case 'K': + end = optarg; + while (end && !invalid) { + switch (*end) { + case 't': + dp->proto |= IS_HTTPS; + break; + case 'h': + dp->proto |= IS_HTTP; + break; + case 'u': + dp->proto |= IS_UDP; + break; + default: + invalid = 1; + continue; + } + end = strchr(end, ','); + if (end) end++; + } + break; + + case 'H':; + if (dp->file_ptr) { + continue; + } + dp->file_ptr = ftob(optarg, &dp->file_size); + if (!dp->file_ptr) { + uniperror("read/parse"); + invalid = 1; + continue; + } + dp->hosts = parse_hosts(dp->file_ptr, dp->file_size); + if (!dp->hosts) { + perror("parse_hosts"); + reset_params(); + return -1; + } + break; + + case 's': + case 'd': + case 'o': + case 'f': + ; + struct part *part = add((void *)&dp->parts, + &dp->parts_n, sizeof(struct part)); + if (!part) { + reset_params(); + return -1; + } + if (parse_offset(part, optarg)) { + invalid = 1; + break; + } + switch (rez) { + case 's': part->m = DESYNC_SPLIT; + break; + case 'd': part->m = DESYNC_DISORDER; + break; + case 'o': part->m = DESYNC_OOB; + break; + case 'f': part->m = DESYNC_FAKE; + } + break; + + case 't': + val = strtol(optarg, &end, 0); + if (val <= 0 || val > 255 || *end) + invalid = 1; + else + dp->ttl = val; + break; + + case 'k': + if (dp->ip_options) { + continue; + } + if (optarg) + dp->ip_options = ftob(optarg, &dp->ip_options_len); + else { + dp->ip_options = ip_option; + dp->ip_options_len = sizeof(ip_option); + } + if (!dp->ip_options) { + uniperror("read/parse"); + invalid = 1; + } + break; + + case 'S': + dp->md5sig = 1; + break; + + case 'n': + if (change_tls_sni(optarg, fake_tls.data, fake_tls.size)) { + fprintf(stderr, "error chsni\n"); + reset_params(); + return -1; + } + printf("sni: %s\n", optarg); + break; + + case 'l': + if (dp->fake_data.data) { + continue; + } + dp->fake_data.data = ftob(optarg, &dp->fake_data.size); + if (!dp->fake_data.data) { + uniperror("read/parse"); + invalid = 1; + } + break; + + case 'e': + if (oob_data.data != oob_char) { + continue; + } + oob_data.data = ftob(optarg, &oob_data.size); + if (!oob_data.data) { + uniperror("read/parse"); + invalid = 1; + } + break; + + case 'M': + end = optarg; + while (end && !invalid) { + switch (*end) { + case 'r': + dp->mod_http |= MH_SPACE; + break; + case 'h': + dp->mod_http |= MH_HMIX; + break; + case 'd': + dp->mod_http |= MH_DMIX; + break; + default: + invalid = 1; + continue; + } + end = strchr(end, ','); + if (end) end++; + } + break; + + case 'r': + part = add((void *)&dp->tlsrec, + &dp->tlsrec_n, sizeof(struct part)); + if (!part) { + reset_params(); + return -1; + } + if (parse_offset(part, optarg) + || part->pos > 0xffff) { + invalid = 1; + break; + } + break; + + case 'a': + val = strtol(optarg, &end, 0); + if (val < 0 || val > INT_MAX || *end) + invalid = 1; + else + dp->udp_fake_count = val; + break; + + case 'V': + val = strtol(optarg, &end, 0); + if (val <= 0 || val > USHRT_MAX) + invalid = 1; + else { + dp->pf[0] = htons(val); + if (*end == '-') { + val = strtol(end + 1, &end, 0); + if (val <= 0 || val > USHRT_MAX) + invalid = 1; + } + if (*end) + invalid = 1; + else + dp->pf[1] = htons(val); + } + break; + + case 'g': + val = strtol(optarg, &end, 0); + if (val <= 0 || val > 255 || *end) + invalid = 1; + else { + params.def_ttl = val; + params.custom_ttl = 1; + } + break; + + case 'w': // + params.sfdelay = strtol(optarg, &end, 0); + if (params.sfdelay < 0 || optarg == end + || params.sfdelay >= 1000 || *end) + invalid = 1; + break; + + case 'W': + params.wait_send = 0; + break; + case 'P': + params.protect_path = optarg; + break; + case 0: + break; + + case '?': + reset_params(); + return -1; + + default: + printf("?: %c\n", rez); + reset_params(); + return -1; + } + } + if (invalid) { + fprintf(stderr, "invalid value: -%c %s\n", rez, optarg); + reset_params(); + return -1; + } + if (dp->hosts || dp->proto || dp->pf[0]) { + dp = add((void *)¶ms.dp, + ¶ms.dp_count, sizeof(struct desync_params)); + if (!dp) { + reset_params(); + return -1; + } + } + + if (params.baddr.sin6_family != AF_INET6) { + params.ipv6 = 0; + } + if (!params.def_ttl) { + if ((params.def_ttl = get_default_ttl()) < 1) { + reset_params(); + return -1; + } + } + params.mempool = mem_pool(0); + if (!params.mempool) { + uniperror("mem_pool"); + reset_params(); + return -1; + } + + return 0; +} diff --git a/app/src/main/cpp/utils.h b/app/src/main/cpp/utils.h index 9846b94..56136d7 100644 --- a/app/src/main/cpp/utils.h +++ b/app/src/main/cpp/utils.h @@ -1,3 +1,4 @@ extern struct params default_params; void reset_params(void); +int parse_args(int argc, char **argv); diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/MainActivity.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/MainActivity.kt index 99b67da..cb9d19d 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/MainActivity.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/MainActivity.kt @@ -19,19 +19,12 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import io.github.dovecoteescapee.byedpi.R -import io.github.dovecoteescapee.byedpi.data.AppStatus -import io.github.dovecoteescapee.byedpi.data.FAILED_BROADCAST -import io.github.dovecoteescapee.byedpi.data.Mode -import io.github.dovecoteescapee.byedpi.data.SENDER -import io.github.dovecoteescapee.byedpi.data.STARTED_BROADCAST -import io.github.dovecoteescapee.byedpi.data.STOPPED_BROADCAST -import io.github.dovecoteescapee.byedpi.data.Sender -import io.github.dovecoteescapee.byedpi.fragments.SettingsFragment +import io.github.dovecoteescapee.byedpi.data.* +import io.github.dovecoteescapee.byedpi.fragments.MainSettingsFragment import io.github.dovecoteescapee.byedpi.databinding.ActivityMainBinding import io.github.dovecoteescapee.byedpi.services.ServiceManager import io.github.dovecoteescapee.byedpi.services.appStatus -import io.github.dovecoteescapee.byedpi.utility.getPreferences -import io.github.dovecoteescapee.byedpi.utility.mode +import io.github.dovecoteescapee.byedpi.utility.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.io.IOException @@ -154,9 +147,9 @@ class MainActivity : AppCompatActivity() { } } - val theme = getPreferences(this) + val theme = getPreferences() .getString("app_theme", null) - SettingsFragment.setTheme(theme ?: "system") + MainSettingsFragment.setTheme(theme ?: "system") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission( @@ -215,7 +208,7 @@ class MainActivity : AppCompatActivity() { } private fun start() { - when (getPreferences(this).mode()) { + when (getPreferences().mode()) { Mode.VPN -> { val intentPrepare = VpnService.prepare(this) if (intentPrepare != null) { @@ -238,9 +231,9 @@ class MainActivity : AppCompatActivity() { Log.i(TAG, "Updating status: $status, $mode") - val preferences = getPreferences(this) - val proxyIp = preferences.getString("byedpi_proxy_ip", null) ?: "127.0.0.1" - val proxyPort = preferences.getString("byedpi_proxy_port", null) ?: "1080" + val preferences = getPreferences() + val proxyIp = preferences.getStringNotNull("byedpi_proxy_ip", "127.0.0.1") + val proxyPort = preferences.getStringNotNull("byedpi_proxy_port", "1080") binding.proxyAddress.text = getString(R.string.proxy_address, proxyIp, proxyPort) when (status) { diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/SettingsActivity.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/SettingsActivity.kt index 5b95624..e48ca95 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/SettingsActivity.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/activities/SettingsActivity.kt @@ -5,7 +5,7 @@ import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import io.github.dovecoteescapee.byedpi.R -import io.github.dovecoteescapee.byedpi.fragments.SettingsFragment +import io.github.dovecoteescapee.byedpi.fragments.MainSettingsFragment import io.github.dovecoteescapee.byedpi.utility.getPreferences class SettingsActivity : AppCompatActivity() { @@ -15,7 +15,7 @@ class SettingsActivity : AppCompatActivity() { supportFragmentManager .beginTransaction() - .replace(R.id.settings, SettingsFragment()) + .replace(R.id.settings, MainSettingsFragment()) .commit() supportActionBar?.setDisplayHomeAsUpEnabled(true) @@ -33,14 +33,11 @@ class SettingsActivity : AppCompatActivity() { } R.id.action_reset_settings -> { - getPreferences(this) - .edit() - .clear() - .apply() + getPreferences().edit().clear().apply() supportFragmentManager .beginTransaction() - .replace(R.id.settings, SettingsFragment()) + .replace(R.id.settings, MainSettingsFragment()) .commit() true } diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxy.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxy.kt index f923a70..bce1ff0 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxy.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxy.kt @@ -41,7 +41,19 @@ class ByeDpiProxy { throw IllegalStateException("Proxy is already running") } - val fd = jniCreateSocket( + val fd = createSocketFromPreferences(preferences) + if (fd < 0) { + return -1 + } + this.fd = fd + fd + } + + private fun createSocketFromPreferences(preferences: ByeDpiProxyPreferences) = + when (preferences) { + is ByeDpiProxyCmdPreferences -> jniCreateSocketWithCommandLine(preferences.args) + + is ByeDpiProxyUIPreferences -> jniCreateSocket( ip = preferences.ip, port = preferences.port, maxConnections = preferences.maxConnections, @@ -49,6 +61,9 @@ class ByeDpiProxy { defaultTtl = preferences.defaultTtl, customTtl = preferences.customTtl, noDomain = preferences.noDomain, + desyncHttp = preferences.desyncHttp, + desyncHttps = preferences.desyncHttps, + desyncUdp = preferences.desyncUdp, desyncMethod = preferences.desyncMethod.ordinal, splitPosition = preferences.splitPosition, splitAtHost = preferences.splitAtHost, @@ -62,15 +77,10 @@ class ByeDpiProxy { tlsRecordSplitPosition = preferences.tlsRecordSplitPosition, tlsRecordSplitAtSni = preferences.tlsRecordSplitAtSni, ) - - if (fd < 0) { - return -1 - } - - this.fd = fd - fd } + private external fun jniCreateSocketWithCommandLine(args: Array): Int + private external fun jniCreateSocket( ip: String, port: Int, @@ -79,6 +89,9 @@ class ByeDpiProxy { defaultTtl: Int, customTtl: Boolean, noDomain: Boolean, + desyncHttp: Boolean, + desyncHttps: Boolean, + desyncUdp: Boolean, desyncMethod: Int, splitPosition: Int, splitAtHost: Boolean, diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxyPreferences.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxyPreferences.kt index d1aa781..dcf295c 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxyPreferences.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/core/ByeDpiProxyPreferences.kt @@ -1,14 +1,48 @@ package io.github.dovecoteescapee.byedpi.core import android.content.SharedPreferences +import io.github.dovecoteescapee.byedpi.utility.getStringNotNull +import io.github.dovecoteescapee.byedpi.utility.shellSplit -class ByeDpiProxyPreferences( +sealed interface ByeDpiProxyPreferences { + companion object { + fun fromSharedPreferences(preferences: SharedPreferences): ByeDpiProxyPreferences = + when (preferences.getBoolean("byedpi_enable_cmd_settings", false)) { + true -> ByeDpiProxyCmdPreferences(preferences) + false -> ByeDpiProxyUIPreferences(preferences) + } + } +} + +class ByeDpiProxyCmdPreferences(val args: Array) : ByeDpiProxyPreferences { + constructor(cmd: String) : this(cmdToArgs(cmd)) + + constructor(preferences: SharedPreferences) : this( + preferences.getStringNotNull( + "byedpi_cmd_args", + "" + ) + ) + + companion object { + private fun cmdToArgs(cmd: String): Array { + val firstArgIndex = cmd.indexOf("-") + val argsStr = (if (firstArgIndex > 0) cmd.substring(firstArgIndex) else cmd).trim() + return arrayOf("ciadpi") + shellSplit(argsStr) + } + } +} + +class ByeDpiProxyUIPreferences( ip: String? = null, port: Int? = null, maxConnections: Int? = null, bufferSize: Int? = null, defaultTtl: Int? = null, noDomain: Boolean? = null, + desyncHttp: Boolean? = null, + desyncHttps: Boolean? = null, + desyncUdp: Boolean? = null, desyncMethod: DesyncMethod? = null, splitPosition: Int? = null, splitAtHost: Boolean? = null, @@ -21,7 +55,7 @@ class ByeDpiProxyPreferences( tlsRecordSplit: Boolean? = null, tlsRecordSplitPosition: Int? = null, tlsRecordSplitAtSni: Boolean? = null, -) { +) : ByeDpiProxyPreferences { val ip: String = ip ?: "127.0.0.1" val port: Int = port ?: 1080 val maxConnections: Int = maxConnections ?: 512 @@ -29,6 +63,9 @@ class ByeDpiProxyPreferences( val defaultTtl: Int = defaultTtl ?: 0 val customTtl: Boolean = defaultTtl != null val noDomain: Boolean = noDomain ?: false + val desyncHttp: Boolean = desyncHttp ?: true + val desyncHttps: Boolean = desyncHttps ?: true + val desyncUdp: Boolean = desyncUdp ?: false val desyncMethod: DesyncMethod = desyncMethod ?: DesyncMethod.Disorder val splitPosition: Int = splitPosition ?: 3 val splitAtHost: Boolean = splitAtHost ?: false @@ -49,6 +86,9 @@ class ByeDpiProxyPreferences( bufferSize = preferences.getString("byedpi_buffer_size", null)?.toIntOrNull(), defaultTtl = preferences.getString("byedpi_default_ttl", null)?.toIntOrNull(), noDomain = preferences.getBoolean("byedpi_no_domain", false), + desyncHttp = preferences.getBoolean("byedpi_desync_http", true), + desyncHttps = preferences.getBoolean("byedpi_desync_https", true), + desyncUdp = preferences.getBoolean("byedpi_desync_udp", false), desyncMethod = preferences.getString("byedpi_desync_method", null) ?.let { DesyncMethod.fromName(it) }, splitPosition = preferences.getString("byedpi_split_position", null)?.toIntOrNull(), @@ -60,9 +100,10 @@ class ByeDpiProxyPreferences( domainMixedCase = preferences.getBoolean("byedpi_domain_mixed_case", false), hostRemoveSpaces = preferences.getBoolean("byedpi_host_remove_spaces", false), tlsRecordSplit = preferences.getBoolean("byedpi_tlsrec_enabled", false), - tlsRecordSplitPosition = preferences.getString("byedpi_tlsrec_position", null)?.toIntOrNull(), + tlsRecordSplitPosition = preferences.getString("byedpi_tlsrec_position", null) + ?.toIntOrNull(), tlsRecordSplitAtSni = preferences.getBoolean("byedpi_tlsrec_at_sni", false), - ) + ) enum class DesyncMethod { None, diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/ByeDpiCommandLineSettingsFragment.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/ByeDpiCommandLineSettingsFragment.kt new file mode 100644 index 0000000..63b4fc9 --- /dev/null +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/ByeDpiCommandLineSettingsFragment.kt @@ -0,0 +1,11 @@ +package io.github.dovecoteescapee.byedpi.fragments + +import android.os.Bundle +import androidx.preference.PreferenceFragmentCompat +import io.github.dovecoteescapee.byedpi.R + +class ByeDpiCommandLineSettingsFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.byedpi_cmd_settings, rootKey) + } +} diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/ByeDpiUISettingsFragment.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/ByeDpiUISettingsFragment.kt new file mode 100644 index 0000000..570e5df --- /dev/null +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/ByeDpiUISettingsFragment.kt @@ -0,0 +1,146 @@ +package io.github.dovecoteescapee.byedpi.fragments + +import android.content.SharedPreferences +import android.os.Bundle +import androidx.preference.* +import io.github.dovecoteescapee.byedpi.R +import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyUIPreferences +import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyUIPreferences.DesyncMethod.* +import io.github.dovecoteescapee.byedpi.utility.* + +class ByeDpiUISettingsFragment : PreferenceFragmentCompat() { + + private val preferenceListener = + SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> + updatePreferences() + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.byedpi_ui_settings, rootKey) + + setEditTextPreferenceListener("byedpi_proxy_ip") { checkIp(it) } + setEditTestPreferenceListenerPort("byedpi_proxy_port") + setEditTestPreferenceListenerInt( + "byedpi_max_connections", + 1, + Short.MAX_VALUE.toInt() + ) + setEditTestPreferenceListenerInt( + "byedpi_buffer_size", + 1, + Int.MAX_VALUE / 4 + ) + setEditTestPreferenceListenerInt("byedpi_default_ttl", 0, 255) + setEditTestPreferenceListenerInt( + "byedpi_split_position", + Int.MIN_VALUE, + Int.MAX_VALUE + ) + setEditTestPreferenceListenerInt("byedpi_fake_ttl", 1, 255) + setEditTestPreferenceListenerInt( + "byedpi_tlsrec_position", + 2 * Short.MIN_VALUE, + 2 * Short.MAX_VALUE, + ) + + findPreferenceNotNull("byedpi_oob_data") + .setOnBindEditTextListener { + it.filters = arrayOf(android.text.InputFilter.LengthFilter(1)) + } + + updatePreferences() + } + + override fun onResume() { + super.onResume() + sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener) + } + + override fun onPause() { + super.onPause() + sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener) + } + + private fun updatePreferences() { + val desyncMethod = + findPreferenceNotNull("byedpi_desync_method") + .value.let { ByeDpiProxyUIPreferences.DesyncMethod.fromName(it) } + + val desyncHttp = findPreferenceNotNull("byedpi_desync_http") + val desyncHttps = findPreferenceNotNull("byedpi_desync_https") + val desyncUdp = findPreferenceNotNull("byedpi_desync_udp") + val splitPosition = findPreferenceNotNull("byedpi_split_position") + val splitAtHost = findPreferenceNotNull("byedpi_split_at_host") + val ttlFake = findPreferenceNotNull("byedpi_fake_ttl") + val fakeSni = findPreferenceNotNull("byedpi_fake_sni") + val oobData = findPreferenceNotNull("byedpi_oob_data") + val hostMixedCase = findPreferenceNotNull("byedpi_host_mixed_case") + val domainMixedCase = findPreferenceNotNull("byedpi_domain_mixed_case") + val hostRemoveSpaces = + findPreferenceNotNull("byedpi_host_remove_spaces") + val splitTlsRec = findPreferenceNotNull("byedpi_tlsrec_enabled") + val splitTlsRecPosition = + findPreferenceNotNull("byedpi_tlsrec_position") + val splitTlsRecAtSni = findPreferenceNotNull("byedpi_tlsrec_at_sni") + + when (desyncMethod) { + None -> { + desyncHttp.isVisible = false + desyncHttps.isVisible = false + desyncUdp.isVisible = false + splitPosition.isVisible = false + splitAtHost.isVisible = false + ttlFake.isVisible = false + fakeSni.isVisible = false + oobData.isVisible = false + hostMixedCase.isVisible = false + domainMixedCase.isVisible = false + hostRemoveSpaces.isVisible = false + } + + else -> { + desyncHttp.isVisible = true + desyncHttps.isVisible = true + desyncUdp.isVisible = true + splitPosition.isVisible = true + splitAtHost.isVisible = true + + val desyncAllProtocols = + !desyncHttp.isChecked && !desyncHttps.isChecked && !desyncUdp.isChecked + + if (desyncAllProtocols || desyncHttp.isChecked) { + hostMixedCase.isVisible = true + domainMixedCase.isVisible = true + hostRemoveSpaces.isVisible = true + } else { + hostMixedCase.isVisible = false + domainMixedCase.isVisible = false + hostRemoveSpaces.isVisible = false + } + + when (desyncMethod) { + Fake -> { + ttlFake.isVisible = true + fakeSni.isVisible = true + oobData.isVisible = false + } + + OOB -> { + ttlFake.isVisible = false + fakeSni.isVisible = false + oobData.isVisible = true + } + + else -> { + ttlFake.isVisible = false + fakeSni.isVisible = false + oobData.isVisible = false + } + } + } + } + + splitTlsRecPosition.isVisible = splitTlsRec.isChecked + splitTlsRecAtSni.isVisible = splitTlsRec.isChecked + } +} diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/MainSettingsFragment.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/MainSettingsFragment.kt new file mode 100644 index 0000000..d8ee026 --- /dev/null +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/MainSettingsFragment.kt @@ -0,0 +1,92 @@ +package io.github.dovecoteescapee.byedpi.fragments + +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatDelegate +import androidx.preference.* +import io.github.dovecoteescapee.byedpi.BuildConfig +import io.github.dovecoteescapee.byedpi.R +import io.github.dovecoteescapee.byedpi.data.Mode +import io.github.dovecoteescapee.byedpi.utility.* + +class MainSettingsFragment : PreferenceFragmentCompat() { + companion object { + private val TAG: String = MainSettingsFragment::class.java.simpleName + + fun setTheme(name: String) = + themeByName(name)?.let { + AppCompatDelegate.setDefaultNightMode(it) + } ?: throw IllegalStateException("Invalid value for app_theme: $name") + + private fun themeByName(name: String): Int? = when (name) { + "system" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + "light" -> AppCompatDelegate.MODE_NIGHT_NO + "dark" -> AppCompatDelegate.MODE_NIGHT_YES + else -> { + Log.w(TAG, "Invalid value for app_theme: $name") + null + } + } + } + + private val preferenceListener = + SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> + updatePreferences() + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.main_settings, rootKey) + + setEditTextPreferenceListener("dns_ip") { checkIp(it) } + + findPreferenceNotNull("app_theme") + .setOnPreferenceChangeListener { _, newValue -> + setTheme(newValue as String) + true + } + + val switchCommandLineSettings = findPreferenceNotNull( + "byedpi_enable_cmd_settings" + ) + val uiSettings = findPreferenceNotNull("byedpi_ui_settings") + val cmdSettings = findPreferenceNotNull("byedpi_cmd_settings") + + val setByeDpiSettingsMode = { enable: Boolean -> + uiSettings.isEnabled = !enable + cmdSettings.isEnabled = enable + } + + setByeDpiSettingsMode(switchCommandLineSettings.isChecked) + + switchCommandLineSettings.setOnPreferenceChangeListener { _, newValue -> + setByeDpiSettingsMode(newValue as Boolean) + true + } + + findPreferenceNotNull("version").summary = BuildConfig.VERSION_NAME + + updatePreferences() + } + + override fun onResume() { + super.onResume() + sharedPreferences?.registerOnSharedPreferenceChangeListener(preferenceListener) + } + + override fun onPause() { + super.onPause() + sharedPreferences?.unregisterOnSharedPreferenceChangeListener(preferenceListener) + } + + private fun updatePreferences() { + val mode = findPreferenceNotNull("byedpi_mode") + .value.let { Mode.fromString(it) } + val dns = findPreferenceNotNull("dns_ip") + + when (mode) { + Mode.VPN -> dns.isVisible = true + Mode.Proxy -> dns.isVisible = false + } + } +} diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/SettingsFragment.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/SettingsFragment.kt deleted file mode 100644 index dc919a5..0000000 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/fragments/SettingsFragment.kt +++ /dev/null @@ -1,131 +0,0 @@ -package io.github.dovecoteescapee.byedpi.fragments - -import android.net.InetAddresses -import android.os.Build -import android.os.Bundle -import android.util.Log -import android.util.Patterns -import android.widget.Toast -import androidx.appcompat.app.AppCompatDelegate -import androidx.preference.DropDownPreference -import androidx.preference.EditTextPreference -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import io.github.dovecoteescapee.byedpi.BuildConfig -import io.github.dovecoteescapee.byedpi.R - -class SettingsFragment : PreferenceFragmentCompat() { - companion object { - private val TAG: String = SettingsFragment::class.java.simpleName - - fun setTheme(name: String): Boolean = when (val theme = themeByName(name)) { - null -> { - Log.w(TAG, "Invalid value for app_theme: $name") - false - } - - else -> { - AppCompatDelegate.setDefaultNightMode(theme) - true - } - } - - private fun themeByName(name: String): Int? = when (name) { - "system" -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - "light" -> AppCompatDelegate.MODE_NIGHT_NO - "dark" -> AppCompatDelegate.MODE_NIGHT_YES - else -> { - Log.w(TAG, "Invalid value for app_theme: $name") - null - } - } - - private fun checkIp(ip: String): Boolean = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - InetAddresses.isNumericAddress(ip) - } else { - // This pattern doesn't not support IPv6 - Patterns.IP_ADDRESS.matcher(ip).matches() - } - } - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.settings, rootKey) - - findPreference("app_theme") - ?.setOnPreferenceChangeListener { preference, newValue -> - setTheme(newValue as String) - } - - setEditTextPreferenceListener("dns_ip") { checkIp(it) } - setEditTextPreferenceListener("byedpi_proxy_ip") { checkIp(it) } - setEditTestPreferenceListenerPort("byedpi_proxy_port") - setEditTestPreferenceListenerInt( - "byedpi_max_connections", - 1, - Short.MAX_VALUE.toInt() - ) - setEditTestPreferenceListenerInt( - "byedpi_buffer_size", - 1, - Int.MAX_VALUE / 4 - ) - setEditTestPreferenceListenerInt("byedpi_default_ttl", 0, 255) - setEditTestPreferenceListenerInt( - "byedpi_split_position", - Int.MIN_VALUE, - Int.MAX_VALUE - ) - setEditTestPreferenceListenerInt("byedpi_fake_ttl", 1, 255) - setEditTestPreferenceListenerInt( - "byedpi_tlsrec_position", - 2 * Short.MIN_VALUE, - 2 * Short.MAX_VALUE, - ) - - findPreference("version")?.summary = - BuildConfig.VERSION_NAME - } - - private fun setEditTestPreferenceListenerPort(key: String) { - setEditTestPreferenceListenerInt(key, 1, 65535) - } - - private fun setEditTestPreferenceListenerInt( - key: String, - min: Int = Int.MIN_VALUE, - max: Int = Int.MAX_VALUE - ) { - setEditTextPreferenceListener(key) { value -> - value.toIntOrNull()?.let { it in min..max } ?: false - } - } - - private fun setEditTextPreferenceListener(key: String, check: (String) -> Boolean) { - findPreference(key) - ?.setOnPreferenceChangeListener { preference, newValue -> - when (newValue) { - is String -> { - val valid = check(newValue) - if (!valid) { - Toast.makeText( - requireContext(), - "Invalid value for ${preference.title}: $newValue", - Toast.LENGTH_SHORT - ).show() - } - valid - } - - else -> { - Log.w( - TAG, - "Invalid type for ${preference.key}: " + - "$newValue has type ${newValue::class.java}" - ) - false - } - } - } - } -} diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiProxyService.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiProxyService.kt index 892ec5c..d3de409 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiProxyService.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiProxyService.kt @@ -10,19 +10,8 @@ import androidx.lifecycle.lifecycleScope import io.github.dovecoteescapee.byedpi.R import io.github.dovecoteescapee.byedpi.core.ByeDpiProxy import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyPreferences -import io.github.dovecoteescapee.byedpi.data.AppStatus -import io.github.dovecoteescapee.byedpi.data.START_ACTION -import io.github.dovecoteescapee.byedpi.data.STOP_ACTION -import io.github.dovecoteescapee.byedpi.data.FAILED_BROADCAST -import io.github.dovecoteescapee.byedpi.data.Mode -import io.github.dovecoteescapee.byedpi.data.SENDER -import io.github.dovecoteescapee.byedpi.data.STARTED_BROADCAST -import io.github.dovecoteescapee.byedpi.data.STOPPED_BROADCAST -import io.github.dovecoteescapee.byedpi.data.Sender -import io.github.dovecoteescapee.byedpi.data.ServiceStatus -import io.github.dovecoteescapee.byedpi.utility.createConnectionNotification -import io.github.dovecoteescapee.byedpi.utility.getPreferences -import io.github.dovecoteescapee.byedpi.utility.registerNotificationChannel +import io.github.dovecoteescapee.byedpi.data.* +import io.github.dovecoteescapee.byedpi.utility.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -159,7 +148,7 @@ class ByeDpiProxyService : LifecycleService() { } private fun getByeDpiPreferences(): ByeDpiProxyPreferences = - ByeDpiProxyPreferences(getPreferences(this)) + ByeDpiProxyPreferences.fromSharedPreferences(getPreferences()) private fun updateStatus(newStatus: ServiceStatus) { Log.d(TAG, "Proxy status changed from $status to $newStatus") diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiVpnService.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiVpnService.kt index 3985f96..45e6714 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiVpnService.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ByeDpiVpnService.kt @@ -15,19 +15,8 @@ import io.github.dovecoteescapee.byedpi.R import io.github.dovecoteescapee.byedpi.activities.MainActivity import io.github.dovecoteescapee.byedpi.core.ByeDpiProxy import io.github.dovecoteescapee.byedpi.core.ByeDpiProxyPreferences -import io.github.dovecoteescapee.byedpi.data.AppStatus -import io.github.dovecoteescapee.byedpi.data.START_ACTION -import io.github.dovecoteescapee.byedpi.data.STOP_ACTION -import io.github.dovecoteescapee.byedpi.data.FAILED_BROADCAST -import io.github.dovecoteescapee.byedpi.data.Mode -import io.github.dovecoteescapee.byedpi.data.SENDER -import io.github.dovecoteescapee.byedpi.data.STARTED_BROADCAST -import io.github.dovecoteescapee.byedpi.data.STOPPED_BROADCAST -import io.github.dovecoteescapee.byedpi.data.Sender -import io.github.dovecoteescapee.byedpi.data.ServiceStatus -import io.github.dovecoteescapee.byedpi.utility.createConnectionNotification -import io.github.dovecoteescapee.byedpi.utility.getPreferences -import io.github.dovecoteescapee.byedpi.utility.registerNotificationChannel +import io.github.dovecoteescapee.byedpi.data.* +import io.github.dovecoteescapee.byedpi.utility.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -189,9 +178,9 @@ class ByeDpiVpnService : LifecycleVpnService() { throw IllegalStateException("VPN field not null") } - val sharedPreferences = getPreferences(this) + val sharedPreferences = getPreferences() val port = sharedPreferences.getString("byedpi_proxy_port", null)?.toInt() ?: 1080 - val dns = sharedPreferences.getString("dns_ip", null) ?: "1.1.1.1" + val dns = sharedPreferences.getStringNotNull("dns_ip", "1.1.1.1") val vpn = createBuilder(dns).establish() ?: throw IllegalStateException("VPN connection failed") @@ -213,7 +202,7 @@ class ByeDpiVpnService : LifecycleVpnService() { } private fun getByeDpiPreferences(): ByeDpiProxyPreferences = - ByeDpiProxyPreferences(getPreferences(this)) + ByeDpiProxyPreferences.fromSharedPreferences(getPreferences()) private fun updateStatus(newStatus: ServiceStatus) { Log.d(TAG, "VPN status changed from $status to $newStatus") diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/LifecycleVpnService.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/LifecycleVpnService.kt index d4bd7e4..d6dc85d 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/LifecycleVpnService.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/LifecycleVpnService.kt @@ -8,7 +8,11 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ServiceLifecycleDispatcher +/** + * Based on @link [androidx.lifecycle.LifecycleService] + */ open class LifecycleVpnService : VpnService(), LifecycleOwner { + @Suppress("LeakingThis") private val dispatcher = ServiceLifecycleDispatcher(this) @CallSuper @@ -48,4 +52,4 @@ open class LifecycleVpnService : VpnService(), LifecycleOwner { override val lifecycle: Lifecycle get() = dispatcher.lifecycle -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/QuickTileService.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/QuickTileService.kt index 3647d6b..d9c2b99 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/QuickTileService.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/QuickTileService.kt @@ -17,13 +17,7 @@ import androidx.core.service.quicksettings.PendingIntentActivityWrapper import androidx.core.service.quicksettings.TileServiceCompat import io.github.dovecoteescapee.byedpi.R import io.github.dovecoteescapee.byedpi.activities.MainActivity -import io.github.dovecoteescapee.byedpi.data.AppStatus -import io.github.dovecoteescapee.byedpi.data.FAILED_BROADCAST -import io.github.dovecoteescapee.byedpi.data.Mode -import io.github.dovecoteescapee.byedpi.data.SENDER -import io.github.dovecoteescapee.byedpi.data.STARTED_BROADCAST -import io.github.dovecoteescapee.byedpi.data.STOPPED_BROADCAST -import io.github.dovecoteescapee.byedpi.data.Sender +import io.github.dovecoteescapee.byedpi.data.* import io.github.dovecoteescapee.byedpi.utility.getPreferences import io.github.dovecoteescapee.byedpi.utility.mode @@ -116,7 +110,7 @@ class QuickTileService : TileService() { val (status) = appStatus when (status) { AppStatus.Halted -> { - val mode = getPreferences(this).mode() + val mode = getPreferences().mode() if (mode == Mode.VPN && VpnService.prepare(this) != null) { updateStatus() @@ -130,4 +124,4 @@ class QuickTileService : TileService() { AppStatus.Running -> ServiceManager.stop(this) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ServiceManager.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ServiceManager.kt index bd88292..696f0b5 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ServiceManager.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/services/ServiceManager.kt @@ -47,4 +47,4 @@ object ServiceManager { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/ArgumentsUtils.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/ArgumentsUtils.kt new file mode 100644 index 0000000..56534e5 --- /dev/null +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/ArgumentsUtils.kt @@ -0,0 +1,41 @@ +package io.github.dovecoteescapee.byedpi.utility + +// Based on https://gist.github.com/raymyers/8077031 +fun shellSplit(string: CharSequence): List { + val tokens: MutableList = ArrayList() + var escaping = false + var quoteChar = ' ' + var quoting = false + var lastCloseQuoteIndex = Int.MIN_VALUE + var current = StringBuilder() + + for (i in string.indices) { + val c = string[i] + + if (escaping) { + current.append(c) + escaping = false + } else if (c == '\\' && !(quoting && quoteChar == '\'')) { + escaping = true + } else if (quoting && c == quoteChar) { + quoting = false + lastCloseQuoteIndex = i + } else if (!quoting && (c == '\'' || c == '"')) { + quoting = true + quoteChar = c + } else if (!quoting && Character.isWhitespace(c)) { + if (current.isNotEmpty() || lastCloseQuoteIndex == i - 1) { + tokens.add(current.toString()) + current = StringBuilder() + } + } else { + current.append(c) + } + } + + if (current.isNotEmpty() || lastCloseQuoteIndex == string.length - 1) { + tokens.add(current.toString()) + } + + return tokens +} diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/PreferencesUtils.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/PreferencesUtils.kt index 92ef3a5..84d8ffb 100644 --- a/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/PreferencesUtils.kt +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/PreferencesUtils.kt @@ -2,14 +2,22 @@ package io.github.dovecoteescapee.byedpi.utility import android.content.Context import android.content.SharedPreferences +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import io.github.dovecoteescapee.byedpi.data.Mode -fun getPreferences(context: Context): SharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context) +val PreferenceFragmentCompat.sharedPreferences + get() = preferenceScreen.sharedPreferences + +fun Context.getPreferences(): SharedPreferences = + PreferenceManager.getDefaultSharedPreferences(this) fun SharedPreferences.getStringNotNull(key: String, defValue: String): String = getString(key, defValue) ?: defValue fun SharedPreferences.mode(): Mode = Mode.fromString(getStringNotNull("byedpi_mode", "vpn")) + +fun PreferenceFragmentCompat.findPreferenceNotNull(key: CharSequence): T = + findPreference(key) ?: throw IllegalStateException("Preference $key not found") diff --git a/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/ValidateUtils.kt b/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/ValidateUtils.kt new file mode 100644 index 0000000..a60929e --- /dev/null +++ b/app/src/main/java/io/github/dovecoteescapee/byedpi/utility/ValidateUtils.kt @@ -0,0 +1,65 @@ +package io.github.dovecoteescapee.byedpi.utility + +import android.net.InetAddresses +import android.os.Build +import android.util.Log +import android.util.Patterns +import android.widget.Toast +import androidx.preference.EditTextPreference +import androidx.preference.PreferenceFragmentCompat + +private const val TAG = "ValidateUtils" + +fun checkIp(ip: String): Boolean = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + InetAddresses.isNumericAddress(ip) + } else { + // This pattern doesn't not support IPv6 + @Suppress("DEPRECATION") + Patterns.IP_ADDRESS.matcher(ip).matches() + } + +fun PreferenceFragmentCompat.setEditTestPreferenceListenerPort(key: String) { + setEditTestPreferenceListenerInt(key, 1, 65535) +} + +fun PreferenceFragmentCompat.setEditTestPreferenceListenerInt( + key: String, + min: Int = Int.MIN_VALUE, + max: Int = Int.MAX_VALUE +) { + setEditTextPreferenceListener(key) { value -> + value.toIntOrNull()?.let { it in min..max } ?: false + } +} + +fun PreferenceFragmentCompat.setEditTextPreferenceListener( + key: String, + check: (String) -> Boolean +) { + findPreferenceNotNull(key) + .setOnPreferenceChangeListener { preference, newValue -> + when (newValue) { + is String -> { + val valid = check(newValue) + if (!valid) { + Toast.makeText( + requireContext(), + "Invalid value for ${preference.title}: $newValue", + Toast.LENGTH_SHORT + ).show() + } + valid + } + + else -> { + Log.w( + TAG, + "Invalid type for ${preference.key}: " + + "$newValue has type ${newValue::class.java}" + ) + false + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0db88a1..d2afc0d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -47,4 +47,14 @@ General ByeDPI About + Command line editor + UI editor + Use command line settings + Desync HTTP + Desync HTTPS + Desync UDP + OOB Data + SNI of fake packet + Proxy + Desync \ No newline at end of file diff --git a/app/src/main/res/xml/byedpi_cmd_settings.xml b/app/src/main/res/xml/byedpi_cmd_settings.xml new file mode 100644 index 0000000..3826e38 --- /dev/null +++ b/app/src/main/res/xml/byedpi_cmd_settings.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/byedpi_ui_settings.xml similarity index 67% rename from app/src/main/res/xml/settings.xml rename to app/src/main/res/xml/byedpi_ui_settings.xml index d7051e4..1a73e15 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/byedpi_ui_settings.xml @@ -1,48 +1,20 @@ + android:tag="byedpi_ui_settings"> + + + + - - - - - - - - - - - - - - + android:title="@string/byedpi_proxy"> + + + + + + - - + + + + + + @@ -156,24 +148,4 @@ - - - - - - - - - - diff --git a/app/src/main/res/xml/main_settings.xml b/app/src/main/res/xml/main_settings.xml new file mode 100644 index 0000000..bfdcc7d --- /dev/null +++ b/app/src/main/res/xml/main_settings.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +