From 079aeaedb0d389d80ee798fe1239cbd878285925 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Mon, 18 Aug 2025 23:35:49 +0800 Subject: [PATCH] fix(sync): support custom koreader sync server hosted in LAN, closes #1800 (#1830) --- apps/readest-app/src-tauri/Cargo.toml | 2 +- .../src-tauri/capabilities/default.json | 6 ++ .../app/library/components/KOSyncSettings.tsx | 17 ++++-- .../src/services/sync/KOSyncClient.ts | 24 +++++++- apps/readest-app/src/utils/network.ts | 55 +++++++++++++++++++ 5 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 apps/readest-app/src/utils/network.ts diff --git a/apps/readest-app/src-tauri/Cargo.toml b/apps/readest-app/src-tauri/Cargo.toml index fbc3a90b..3d34cc45 100644 --- a/apps/readest-app/src-tauri/Cargo.toml +++ b/apps/readest-app/src-tauri/Cargo.toml @@ -41,7 +41,7 @@ tauri-plugin-log = "2" tauri-plugin-fs = "2" tauri-plugin-dialog = "2" tauri-plugin-os = "2" -tauri-plugin-http = "2" +tauri-plugin-http = { version = "2", features = ["dangerous-settings"] } tauri-plugin-shell = "2" tauri-plugin-process = "2" tauri-plugin-oauth = "2" diff --git a/apps/readest-app/src-tauri/capabilities/default.json b/apps/readest-app/src-tauri/capabilities/default.json index 86131566..aad2a58f 100644 --- a/apps/readest-app/src-tauri/capabilities/default.json +++ b/apps/readest-app/src-tauri/capabilities/default.json @@ -87,6 +87,12 @@ }, { "url": "https://translate.googleapis.com" + }, + { + "url": "http://*:*" + }, + { + "url": "https://*:*" } ] }, diff --git a/apps/readest-app/src/app/library/components/KOSyncSettings.tsx b/apps/readest-app/src/app/library/components/KOSyncSettings.tsx index 9b155eb8..1fd5c55c 100644 --- a/apps/readest-app/src/app/library/components/KOSyncSettings.tsx +++ b/apps/readest-app/src/app/library/components/KOSyncSettings.tsx @@ -16,6 +16,7 @@ import { getOSPlatform } from '@/utils/misc'; type Option = { value: string; label: string; + disabled?: boolean; }; type SelectProps = { @@ -43,8 +44,8 @@ const StyledSelect: React.FC = ({ )} disabled={disabled} > - {options.map(({ value, label }) => ( - ))} @@ -83,6 +84,12 @@ export const KOSyncSettingsWindow: React.FC = () => { // Get the OS name once useEffect(() => { + const formatOsName = (name: string): string => { + if (!name) return ''; + if (name.toLowerCase() === 'macos') return 'macOS'; + return name.charAt(0).toUpperCase() + name.slice(1); + }; + const getOsName = async () => { let name = ''; if (appService?.appPlatform === 'tauri') { @@ -93,7 +100,7 @@ export const KOSyncSettingsWindow: React.FC = () => { name = platform; } } - setOsName(name ? name.charAt(0).toUpperCase() + name.slice(1) : ''); + setOsName(formatOsName(name)); }; getOsName(); }, [appService]); @@ -283,7 +290,7 @@ export const KOSyncSettingsWindow: React.FC = () => { onChange={handleChecksumMethodChange} options={[ { value: 'binary', label: _('File Content (recommended)') }, - { value: 'filename', label: _('File Name') }, + { value: 'filename', label: _('File Name'), disabled: true }, ]} /> @@ -334,6 +341,7 @@ export const KOSyncSettingsWindow: React.FC = () => { type='text' placeholder='https://koreader.sync.server' className='input input-bordered h-12 w-full focus:outline-none focus:ring-0' + spellCheck='false' value={url} onChange={(e) => setUrl(e.target.value)} /> @@ -346,6 +354,7 @@ export const KOSyncSettingsWindow: React.FC = () => { type='text' placeholder={_('Your Username')} className='input input-bordered h-12 w-full focus:outline-none focus:ring-0' + spellCheck='false' value={username} onChange={(e) => setUsername(e.target.value)} /> diff --git a/apps/readest-app/src/services/sync/KOSyncClient.ts b/apps/readest-app/src/services/sync/KOSyncClient.ts index 7e28439d..87961901 100644 --- a/apps/readest-app/src/services/sync/KOSyncClient.ts +++ b/apps/readest-app/src/services/sync/KOSyncClient.ts @@ -1,8 +1,10 @@ import { md5 } from 'js-md5'; import { Book } from '@/types/book'; import { KoreaderSyncChecksumMethod } from '@/types/settings'; -import { getAPIBaseUrl } from '../environment'; +import { fetch as tauriFetch } from '@tauri-apps/plugin-http'; import { KoSyncProxyPayload } from '@/types/kosync'; +import { isLanAddress } from '@/utils/network'; +import { getAPIBaseUrl, isTauriAppPlatform } from '../environment'; /** * Interface for KOSync progress response from the server @@ -23,6 +25,7 @@ export class KOSyncClient { private checksumMethod: KoreaderSyncChecksumMethod; private deviceId: string; private deviceName: string; + private isLanServer: boolean; constructor( serverUrl: string, @@ -38,6 +41,7 @@ export class KOSyncClient { this.checksumMethod = checksumMethod; this.deviceId = deviceId; this.deviceName = deviceName; + this.isLanServer = isLanAddress(this.serverUrl); } private async request( @@ -57,6 +61,24 @@ export class KOSyncClient { headers.set('X-Auth-Key', this.userkey); } + if (this.isLanServer) { + const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch; + const directUrl = `${this.serverUrl}${endpoint}`; + + return fetch(directUrl, { + method, + headers: { + accept: 'application/vnd.koreader.v1+json', + ...Object.fromEntries(headers.entries()), + }, + body, + danger: { + acceptInvalidCerts: true, + acceptInvalidHostnames: true, + }, + }); + } + const proxyUrl = `${getAPIBaseUrl()}/kosync`; const proxyBody: KoSyncProxyPayload = { diff --git a/apps/readest-app/src/utils/network.ts b/apps/readest-app/src/utils/network.ts new file mode 100644 index 00000000..124b2bd8 --- /dev/null +++ b/apps/readest-app/src/utils/network.ts @@ -0,0 +1,55 @@ +export const isLanAddress = (url: string) => { + try { + const urlObj = new URL(url); + const hostname = urlObj.hostname; + + if (hostname === 'localhost' || hostname === '127.0.0.1') { + return true; + } + + // Check for IPv4 private ranges + const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + const match = hostname.match(ipv4Regex); + + if (match) { + const [, a, b, c, d] = match.map(Number); + if (a === undefined || b === undefined || c === undefined || d === undefined) { + return false; + } + + // Validate IP format + if (a > 255 || b > 255 || c > 255 || d > 255) { + return false; + } + + // Check private IP ranges: + // 10.0.0.0/8 (10.0.0.0 to 10.255.255.255) + if (a === 10) return true; + + // 172.16.0.0/12 (172.16.0.0 to 172.31.255.255) + if (a === 172 && b >= 16 && b <= 31) return true; + + // 192.168.0.0/16 (192.168.0.0 to 192.168.255.255) + if (a === 192 && b === 168) return true; + + // 169.254.0.0/16 (link-local addresses) + if (a === 169 && b === 254) return true; + } + + // Check for IPv6 private addresses (simplified check) + if (hostname.includes(':')) { + if ( + hostname.startsWith('::1') || + hostname.startsWith('fe80:') || + hostname.startsWith('fc00:') || + hostname.startsWith('fd00:') + ) { + return true; + } + } + + return false; + } catch { + return false; + } +};