Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.rajarsheechatterjee.NativeNetwork

import java.net.Inet6Address
import java.net.InetAddress

internal object LocalAddressClassifier {
private val localDnsSuffixes = listOf(
".localhost",
".local",
".lan",
".home",
".home.arpa",
".internal",
)

fun isLocalHostname(value: String): Boolean {
val host = value.trim().trimEnd('.').lowercase()
return host == "localhost" ||
host.isNotEmpty() && '.' !in host && ':' !in host ||
localDnsSuffixes.any(host::endsWith)
}

fun isIpLiteral(value: String): Boolean {
val host = value.trim().removePrefix("[").removeSuffix("]")
return ':' in host || host.isIpv4Address()
}

fun isLocalAddress(address: InetAddress): Boolean {
if (address.isAnyLocalAddress ||
address.isLoopbackAddress ||
address.isLinkLocalAddress ||
address.isSiteLocalAddress
) {
return true
}

val bytes = address.address
if (address is Inet6Address) {
return bytes.isNotEmpty() && (bytes[0].toInt() and 0xFE) == 0xFC
}
if (bytes.size != 4) return false

val first = bytes[0].toInt() and 0xFF
val second = bytes[1].toInt() and 0xFF
return first == 0 ||
first == 10 ||
first == 127 ||
first == 169 && second == 254 ||
first == 172 && second in 16..31 ||
first == 192 && second == 168 ||
first == 100 && second in 64..127
}

private fun String.isIpv4Address(): Boolean {
val octets = split('.')
return octets.size == 4 && octets.all { octet ->
octet.isNotEmpty() &&
octet.length <= 3 &&
octet.all { it in '0'..'9' } &&
octet.toInt() in 0..255
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,11 @@ internal class LocalAwareDns(
private val delegate: Dns,
) : Dns {
override fun lookup(hostname: String): List<InetAddress> =
if (hostname.isLocalHost()) {
if (LocalAddressClassifier.isLocalHostname(hostname) ||
LocalAddressClassifier.isIpLiteral(hostname)
) {
Dns.SYSTEM.lookup(hostname)
Comment thread
aiko-chan-ai marked this conversation as resolved.
} else {
delegate.lookup(hostname)
}
}

private fun String.isLocalHost(): Boolean {
val host = trimEnd('.').lowercase()
return host == "localhost" ||
'.' !in host ||
':' in host ||
host.isIpv4Address() ||
LOCAL_DNS_SUFFIXES.any(host::endsWith)
}

private fun String.isIpv4Address(): Boolean {
val octets = split('.')
return octets.size == 4 && octets.all { octet ->
octet.isNotEmpty() &&
octet.length <= 3 &&
octet.all { it in '0'..'9' } &&
octet.toInt() in 0..255
}
}

private val LOCAL_DNS_SUFFIXES = listOf(
".localhost",
".local",
".lan",
".home",
".home.arpa",
".internal",
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,40 @@ import android.webkit.WebView
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.modules.network.OkHttpClientProvider
import com.lnreader.spec.NativeNetworkSpec
import java.io.File
import okhttp3.Headers

class NativeNetworkModule(context: ReactApplicationContext) : NativeNetworkSpec(context) {
private val networkPreferences = reactApplicationContext.getSharedPreferences(
NETWORK_PREFERENCES,
Context.MODE_PRIVATE,
)

override fun setDohProvider(providerId: String) {
require(DohProvider.isSupported(providerId)) {
"Unsupported DNS-over-HTTPS provider: $providerId"
}
reactApplicationContext
.getSharedPreferences(NETWORK_PREFERENCES, Context.MODE_PRIVATE)
.edit()
networkPreferences.edit()
.putString(DOH_PROVIDER_KEY, providerId)
.apply()
}

override fun setNetworkMode(mode: String) {
require(NetworkMode.isSupported(mode)) { "Unsupported network mode: $mode" }
networkPreferences.edit()
.putString(NETWORK_MODE_KEY, mode)
.apply()
refreshRouting()
}

override fun getNetworkMode(): String = readNetworkMode(reactApplicationContext).id

private fun refreshRouting() {
OkHttpClientProvider.getOkHttpClient().connectionPool.evictAll()
}

override fun isUserAgentValid(value: String): Boolean {
if (value.isBlank()) return false
return runCatching {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import com.facebook.react.modules.network.OkHttpClientFactory
import com.facebook.react.modules.network.OkHttpClientProvider
import java.io.File
import java.net.Proxy
import okhttp3.Cache
import okhttp3.OkHttpClient

Expand All @@ -23,14 +24,24 @@ class NetworkClientFactory(context: Context) : OkHttpClientFactory {
val baseClient = OkHttpClientProvider.createClientBuilder()
.cache(cache)
.build()
val providerId = applicationContext
.getSharedPreferences(NETWORK_PREFERENCES, Context.MODE_PRIVATE)
val preferences = applicationContext.getSharedPreferences(
NETWORK_PREFERENCES,
Context.MODE_PRIVATE,
)
val providerId = preferences
.getString(DOH_PROVIDER_KEY, DISABLED_DOH_PROVIDER)
.orEmpty()
val provider = DohProvider.fromId(providerId) ?: return baseClient
val provider = DohProvider.fromId(providerId)
val clientBuilder = baseClient.newBuilder()
.socketFactory(RoutingSocketFactory(applicationContext))

return baseClient.newBuilder()
.dns(provider.createDns(baseClient))
.build()
if (provider != null) {
val bootstrapClient = baseClient.newBuilder()
.proxy(Proxy.NO_PROXY)
.build()
clientBuilder.dns(provider.createDns(bootstrapClient))
}

return clientBuilder.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.rajarsheechatterjee.NativeNetwork

import android.content.Context

internal const val NETWORK_MODE_KEY = "network_mode"

internal enum class NetworkMode(val id: String) {
DIRECT("direct"),
DPI_BYPASS("dpi_bypass");

companion object {
fun fromId(id: String?) = entries.find { it.id == id } ?: DIRECT
fun isSupported(id: String) = entries.any { it.id == id }
}
}

internal fun readNetworkMode(context: Context): NetworkMode = NetworkMode.fromId(
context.getSharedPreferences(NETWORK_PREFERENCES, Context.MODE_PRIVATE)
.getString(NETWORK_MODE_KEY, null),
)
2 changes: 2 additions & 0 deletions specs/NativeNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
setDohProvider(providerId: string): void;
setNetworkMode(mode: string): void;
getNetworkMode(): string;
isUserAgentValid(value: string): boolean;
clearWebViewData(): Promise<void>;
}
Expand Down
23 changes: 22 additions & 1 deletion src/screens/settings/SettingsAdvancedScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import { NOVEL_UPDATE_RANDOM_KEY } from '@hooks/persisted/useUpdates';
import { AdvancedSettingsScreenProps } from '@navigators/types';
import { store } from '@plugins/helpers/storage';
import CookieManager from '@preeternal/react-native-cookie-manager';
import { useFocusEffect } from '@react-navigation/native';
import { getDohProviderName } from '@services/network/doh';
import { getNetworkMode, setNetworkMode } from '@services/network/settings';
import NativeLocalServer from '@specs/NativeLocalServer';
import NativeNetwork from '@specs/NativeNetwork';
import { getString } from '@strings/translations';
import { MMKVStorage } from '@utils/mmkv/mmkv';
import { showToast } from '@utils/showToast';
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { ScrollView, StyleSheet, View } from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller';
import { Portal, Text, TextInput } from 'react-native-paper';
Expand All @@ -37,6 +39,13 @@ const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => {
const appSettings = useAppSettings();
const { verboseLogging, setAppSettings } = appSettings;
const [userAgentInput, setUserAgentInput] = useState(userAgent);
const [networkMode, setCurrentNetworkMode] = useState(getNetworkMode);

useFocusEffect(
useCallback(() => {
setCurrentNetworkMode(getNetworkMode());
}, []),
);

/**
* Confirm Clear Database Dialog
Expand Down Expand Up @@ -201,6 +210,18 @@ const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => {
onPress={showDohProviderModal}
theme={theme}
/>
<SettingSwitch
label={getString('advancedSettingsScreen.bypassDpi')}
description={getString('advancedSettingsScreen.bypassDpiDesc')}
value={networkMode === 'dpi_bypass'}
onPress={() => {
const mode =
networkMode === 'dpi_bypass' ? 'direct' : 'dpi_bypass';
setNetworkMode(mode);
setCurrentNetworkMode(mode);
}}
theme={theme}
/>
<List.Item
title={getString('advancedSettingsScreen.userAgent')}
description={userAgent}
Expand Down
10 changes: 10 additions & 0 deletions src/services/network/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import NativeNetwork from '@specs/NativeNetwork';

export type NetworkMode = 'direct' | 'dpi_bypass';

export const getNetworkMode = (): NetworkMode =>
NativeNetwork.getNetworkMode() === 'dpi_bypass' ? 'dpi_bypass' : 'direct';

export const setNetworkMode = (mode: NetworkMode) => {
NativeNetwork.setNetworkMode(mode);
};
6 changes: 4 additions & 2 deletions strings/languages/en/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"webViewDataClearFailed": "Failed to clear WebView data",
"dnsOverHttps": "DNS over HTTPS (DoH)",
"disabled": "Disabled",
"bypassDpi": "Bypass internet censorship",
"bypassDpiDesc": "(Experimental) Split initial HTTP and TLS traffic to bypass DPI filtering.",
"resetUserAgent": "Reset default User Agent",
"invalidUserAgent": "Invalid User-Agent value",
"storageUsage": "Storage usage",
Expand All @@ -66,11 +68,11 @@
"recreateDBIndexesDesc": "Recreates DB indexes. This may improve performance on slower devices.",
"developer": "Developer",
"allowCloudflareBypass": "Allow plugins to use Cloudflare bypass API",
"allowCloudflareBypassDesc": "Enables the Cloudflare Turnstile bypass API for plugins. Requires app restart.",
"allowCloudflareBypassDesc": "Enables the Cloudflare Turnstile bypass API for plugins.",
"hideCloudflareOverlay": "Hide WebView overlay when bypassing CF",
Comment on lines 70 to 72
"hideCloudflareOverlayDesc": "Hides the Cloudflare WebView while bypassing. Uncheck to see the solving process.",
"allowProxyAPI": "Allow plugins to use Proxy API",
"allowProxyAPIDesc": "Enables the LocalHttpServer Proxy API used by plugins to bypass CORS. Takes effect immediately.",
"allowProxyAPIDesc": "Enables the LocalHttpServer Proxy API used by plugins to bypass CORS.",
"verboseLogging": "Verbose Logging",
"verboseLoggingDesc": "Print detailed logs including system logs (reduces performance)",
"restartRequiredToast": "Please restart the app to apply changes"
Expand Down
6 changes: 4 additions & 2 deletions strings/languages/vi_VN/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"webViewDataClearFailed": "Không thể xóa dữ liệu WebView",
"dnsOverHttps": "DNS qua HTTPS (DoH)",
"disabled": "Đã tắt",
"bypassDpi": "Vượt kiểm duyệt Internet",
"bypassDpiDesc": "(Thử nghiệm) Chia nhỏ lưu lượng HTTP và TLS ban đầu để vượt bộ lọc DPI.",
"resetUserAgent": "Đặt lại User Agent mặc định",
"invalidUserAgent": "Giá trị User-Agent không hợp lệ",
"storageUsage": "Kho chứa chiếm dụng",
Expand All @@ -66,11 +68,11 @@
"recreateDBIndexesDesc": "Tạo lại DB indexes. Việc này có thể cải thiện hiệu năng trên các thiết bị chậm hơn.",
"developer": "Nhà phát triển",
"allowCloudflareBypass": "Cho phép plugin sử dụng API bypass Cloudflare",
"allowCloudflareBypassDesc": "Bật tính năng vượt qua Cloudflare Turnstile cho các plugin. Yêu cầu khởi động lại ứng dụng.",
"allowCloudflareBypassDesc": "Bật tính năng vượt qua Cloudflare Turnstile cho các plugin.",
"hideCloudflareOverlay": "Ẩn overlay WebView khi bypass CF",
Comment on lines 69 to 72
"hideCloudflareOverlayDesc": "Ẩn giao diện giải Cloudflare. Tắt đi nếu bạn muốn xem quá trình giải.",
"allowProxyAPI": "Cho phép plugin sử dụng Proxy API",
"allowProxyAPIDesc": "Bật tính năng Proxy API cho LocalHttpServer để giúp plugin vượt qua CORS. Áp dụng ngay lập tức.",
"allowProxyAPIDesc": "Bật tính năng Proxy API cho LocalHttpServer để giúp plugin vượt qua CORS.",
"verboseLogging": "Ghi nhật ký chi tiết",
"verboseLoggingDesc": "In các ghi chép tường tận đến cả hệ thống (giảm hiệu suất)",
"restartRequiredToast": "Vui lòng khởi động lại ứng dụng để áp dụng thay đổi"
Expand Down
2 changes: 2 additions & 0 deletions strings/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export interface StringMap {
'advancedSettingsScreen.webViewDataClearFailed': 'string';
'advancedSettingsScreen.dnsOverHttps': 'string';
'advancedSettingsScreen.disabled': 'string';
'advancedSettingsScreen.bypassDpi': 'string';
'advancedSettingsScreen.bypassDpiDesc': 'string';
'advancedSettingsScreen.resetUserAgent': 'string';
'advancedSettingsScreen.invalidUserAgent': 'string';
'advancedSettingsScreen.storageUsage': 'string';
Expand Down
Loading