fix(sync): support custom koreader sync server hosted in LAN, closes #1800 (#1830)

This commit is contained in:
Huang Xin
2025-08-18 23:35:49 +08:00
committed by GitHub
parent bfacadb964
commit 079aeaedb0
5 changed files with 98 additions and 6 deletions
+1 -1
View File
@@ -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"
@@ -87,6 +87,12 @@
},
{
"url": "https://translate.googleapis.com"
},
{
"url": "http://*:*"
},
{
"url": "https://*:*"
}
]
},
@@ -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<SelectProps> = ({
)}
disabled={disabled}
>
{options.map(({ value, label }) => (
<option key={value} value={value}>
{options.map(({ value, label, disabled = false }) => (
<option key={value} value={value} disabled={disabled}>
{label}
</option>
))}
@@ -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 },
]}
/>
</div>
@@ -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)}
/>
@@ -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 = {
+55
View File
@@ -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;
}
};