mirror of
https://github.com/OpenListTeam/OpenList-Desktop.git
synced 2025-11-26 03:28:31 +08:00
feat: add Windows firewall management for openlist port #49
This commit is contained in:
@@ -41,7 +41,7 @@
|
|||||||
"check:all": "yarn check:frontend && yarn check:rust:all",
|
"check:all": "yarn check:frontend && yarn check:rust:all",
|
||||||
"fix:rust": "cd src-tauri && cargo fmt --all && cargo clippy --all-targets --all-features --fix --allow-dirty",
|
"fix:rust": "cd src-tauri && cargo fmt --all && cargo clippy --all-targets --all-features --fix --allow-dirty",
|
||||||
"fix:frontend": "yarn lint:fix",
|
"fix:frontend": "yarn lint:fix",
|
||||||
"fix:all": "yarn fix:frontend && yarn fix:rust"
|
"fix:all": "yarn fix:frontend && yarn fix:rust && yarn i18n:check"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"commitizen": {
|
"commitizen": {
|
||||||
|
|||||||
120
src-tauri/src/cmd/firewall.rs
Normal file
120
src-tauri/src/cmd/firewall.rs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
use std::process::{Command, ExitStatus};
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use deelevate::{PrivilegeLevel, Token};
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use runas::Command as RunasCommand;
|
||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
use crate::object::structs::AppState;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const RULE: &str = "OpenList Core";
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn netsh_with_elevation(args: &[String]) -> Result<ExitStatus, String> {
|
||||||
|
let token = Token::with_current_process().map_err(|e| format!("token: {e}"))?;
|
||||||
|
let elevated = !matches!(
|
||||||
|
token
|
||||||
|
.privilege_level()
|
||||||
|
.map_err(|e| format!("privilege: {e}"))?,
|
||||||
|
PrivilegeLevel::NotPrivileged
|
||||||
|
);
|
||||||
|
|
||||||
|
if elevated {
|
||||||
|
Command::new("netsh")
|
||||||
|
.args(args)
|
||||||
|
.creation_flags(0x08000000)
|
||||||
|
.status()
|
||||||
|
} else {
|
||||||
|
RunasCommand::new("netsh").args(args).show(false).status()
|
||||||
|
}
|
||||||
|
.map_err(|e| format!("netsh: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn firewall_rule(verb: &str, port: Option<u16>) -> Result<bool, String> {
|
||||||
|
let mut args: Vec<String> = vec![
|
||||||
|
"advfirewall".into(),
|
||||||
|
"firewall".into(),
|
||||||
|
verb.into(),
|
||||||
|
"rule".into(),
|
||||||
|
format!("name={RULE}"),
|
||||||
|
];
|
||||||
|
if let Some(p) = port {
|
||||||
|
args.extend([
|
||||||
|
"dir=in".into(),
|
||||||
|
"action=allow".into(),
|
||||||
|
"protocol=TCP".into(),
|
||||||
|
format!("localport={p}"),
|
||||||
|
"description=Allow OpenList Core web interface access".into(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
Ok(netsh_with_elevation(&args)?.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn rule_stdout() -> Result<Option<String>, String> {
|
||||||
|
let out = Command::new("netsh")
|
||||||
|
.args([
|
||||||
|
"advfirewall",
|
||||||
|
"firewall",
|
||||||
|
"show",
|
||||||
|
"rule",
|
||||||
|
&format!("name={RULE}"),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("netsh: {e}"))?;
|
||||||
|
if out.status.success() {
|
||||||
|
Ok(Some(String::from_utf8_lossy(&out.stdout).into()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn firewall_rule(_: &str, _: Option<u16>) -> Result<bool, String> {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn rule_stdout() -> Result<Option<String>, String> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_firewall_rule(state: State<'_, AppState>) -> Result<bool, String> {
|
||||||
|
let port = state
|
||||||
|
.app_settings
|
||||||
|
.read()
|
||||||
|
.clone()
|
||||||
|
.ok_or("read settings")?
|
||||||
|
.openlist
|
||||||
|
.port;
|
||||||
|
|
||||||
|
if let Some(out) = rule_stdout()? {
|
||||||
|
Ok(out.contains(&port.to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn add_firewall_rule(state: State<'_, AppState>) -> Result<bool, String> {
|
||||||
|
let port = state
|
||||||
|
.app_settings
|
||||||
|
.read()
|
||||||
|
.clone()
|
||||||
|
.ok_or("read settings")?
|
||||||
|
.openlist
|
||||||
|
.port;
|
||||||
|
|
||||||
|
let _ = firewall_rule("delete", None);
|
||||||
|
firewall_rule("add", Some(port))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn remove_firewall_rule(_state: State<'_, AppState>) -> Result<bool, String> {
|
||||||
|
firewall_rule("delete", None)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod binary;
|
pub mod binary;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod custom_updater;
|
pub mod custom_updater;
|
||||||
|
pub mod firewall;
|
||||||
pub mod http_api;
|
pub mod http_api;
|
||||||
pub mod logs;
|
pub mod logs;
|
||||||
pub mod openlist_core;
|
pub mod openlist_core;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use cmd::custom_updater::{
|
|||||||
check_for_updates, download_update, get_current_version, install_update_and_restart,
|
check_for_updates, download_update, get_current_version, install_update_and_restart,
|
||||||
is_auto_check_enabled, set_auto_check_enabled,
|
is_auto_check_enabled, set_auto_check_enabled,
|
||||||
};
|
};
|
||||||
|
use cmd::firewall::{add_firewall_rule, check_firewall_rule, remove_firewall_rule};
|
||||||
use cmd::http_api::{
|
use cmd::http_api::{
|
||||||
delete_process, get_process_list, restart_process, start_process, stop_process, update_process,
|
delete_process, get_process_list, restart_process, start_process, stop_process, update_process,
|
||||||
};
|
};
|
||||||
@@ -159,6 +160,9 @@ pub fn run() {
|
|||||||
check_service_status,
|
check_service_status,
|
||||||
stop_service,
|
stop_service,
|
||||||
start_service,
|
start_service,
|
||||||
|
check_firewall_rule,
|
||||||
|
add_firewall_rule,
|
||||||
|
remove_firewall_rule,
|
||||||
check_for_updates,
|
check_for_updates,
|
||||||
download_update,
|
download_update,
|
||||||
install_update_and_restart,
|
install_update_and_restart,
|
||||||
|
|||||||
@@ -104,6 +104,13 @@ export class TauriAPI {
|
|||||||
listen: (cb: (action: string) => void) => listen('tray-core-action', e => cb(e.payload as string))
|
listen: (cb: (action: string) => void) => listen('tray-core-action', e => cb(e.payload as string))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Firewall management ---
|
||||||
|
static firewall = {
|
||||||
|
check: (): Promise<boolean> => call('check_firewall_rule'),
|
||||||
|
add: (): Promise<boolean> => call('add_firewall_rule'),
|
||||||
|
remove: (): Promise<boolean> => call('remove_firewall_rule')
|
||||||
|
}
|
||||||
|
|
||||||
// --- Update management ---
|
// --- Update management ---
|
||||||
static updater = {
|
static updater = {
|
||||||
check: (): Promise<UpdateCheck> => call('check_for_updates'),
|
check: (): Promise<UpdateCheck> => call('check_for_updates'),
|
||||||
|
|||||||
@@ -75,6 +75,28 @@
|
|||||||
<input type="checkbox" v-model="settings.openlist.auto_launch" @change="handleAutoLaunchToggle" />
|
<input type="checkbox" v-model="settings.openlist.auto_launch" @change="handleAutoLaunchToggle" />
|
||||||
<span class="toggle-text">{{ t('dashboard.quickActions.autoLaunch') }}</span>
|
<span class="toggle-text">{{ t('dashboard.quickActions.autoLaunch') }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<!-- Windows Firewall Management-->
|
||||||
|
<button
|
||||||
|
v-if="isWindows"
|
||||||
|
@click="toggleFirewallRule"
|
||||||
|
:class="['firewall-toggle-btn', { active: firewallEnabled }]"
|
||||||
|
:disabled="firewallLoading"
|
||||||
|
:title="
|
||||||
|
firewallEnabled
|
||||||
|
? t('dashboard.quickActions.firewall.disable')
|
||||||
|
: t('dashboard.quickActions.firewall.enable')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Shield :size="18" />
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
firewallEnabled
|
||||||
|
? t('dashboard.quickActions.firewall.disable')
|
||||||
|
: t('dashboard.quickActions.firewall.enable')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,13 +104,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAppStore } from '../../stores/app'
|
import { useAppStore } from '../../stores/app'
|
||||||
import { useRcloneStore } from '../../stores/rclone'
|
import { useRcloneStore } from '../../stores/rclone'
|
||||||
import { useTranslation } from '../../composables/useI18n'
|
import { useTranslation } from '../../composables/useI18n'
|
||||||
import Card from '../ui/Card.vue'
|
import Card from '../ui/Card.vue'
|
||||||
import { Play, Square, RotateCcw, ExternalLink, Settings, HardDrive, Key } from 'lucide-vue-next'
|
import { Play, Square, RotateCcw, ExternalLink, Settings, HardDrive, Key, Shield } from 'lucide-vue-next'
|
||||||
import { TauriAPI } from '@/api/tauri'
|
import { TauriAPI } from '@/api/tauri'
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -100,6 +122,12 @@ const isCoreRunning = computed(() => appStore.isCoreRunning)
|
|||||||
const settings = computed(() => appStore.settings)
|
const settings = computed(() => appStore.settings)
|
||||||
let statusCheckInterval: number | null = null
|
let statusCheckInterval: number | null = null
|
||||||
|
|
||||||
|
const firewallEnabled = ref(false)
|
||||||
|
const firewallLoading = ref(false)
|
||||||
|
const isWindows = computed(() => {
|
||||||
|
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'win32'
|
||||||
|
})
|
||||||
|
|
||||||
const serviceButtonIcon = computed(() => {
|
const serviceButtonIcon = computed(() => {
|
||||||
return isCoreRunning.value ? Square : Play
|
return isCoreRunning.value ? Square : Play
|
||||||
})
|
})
|
||||||
@@ -274,6 +302,80 @@ const stopBackend = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkFirewallStatus = async () => {
|
||||||
|
if (!isWindows.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
firewallEnabled.value = await TauriAPI.firewall.check()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check firewall status:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFirewallRule = async () => {
|
||||||
|
if (!isWindows.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
firewallLoading.value = true
|
||||||
|
|
||||||
|
if (firewallEnabled.value) {
|
||||||
|
await TauriAPI.firewall.remove()
|
||||||
|
firewallEnabled.value = false
|
||||||
|
showNotification('success', t('dashboard.quickActions.firewall.removed'))
|
||||||
|
} else {
|
||||||
|
await TauriAPI.firewall.add()
|
||||||
|
firewallEnabled.value = true
|
||||||
|
showNotification('success', t('dashboard.quickActions.firewall.added'))
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to toggle firewall rule:', error)
|
||||||
|
const message = firewallEnabled.value
|
||||||
|
? t('dashboard.quickActions.firewall.failedToRemove')
|
||||||
|
: t('dashboard.quickActions.firewall.failedToAdd')
|
||||||
|
showNotification('error', message + ': ' + (error.message || error))
|
||||||
|
} finally {
|
||||||
|
firewallLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showNotification = (type: 'success' | 'error', message: string) => {
|
||||||
|
const notification = document.createElement('div')
|
||||||
|
const bgColor =
|
||||||
|
type === 'success'
|
||||||
|
? 'linear-gradient(135deg, rgb(16, 185, 129), rgb(5, 150, 105))'
|
||||||
|
: 'linear-gradient(135deg, rgb(239, 68, 68), rgb(220, 38, 38))'
|
||||||
|
const icon = type === 'success' ? '✓' : '⚠'
|
||||||
|
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div style="
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: ${bgColor};
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 10000;
|
||||||
|
font-weight: 500;
|
||||||
|
max-width: 300px;
|
||||||
|
word-break: break-word;
|
||||||
|
">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<div style="font-size: 18px;">${icon}</div>
|
||||||
|
<div style="font-size: 14px;">${message}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
document.body.appendChild(notification)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification)
|
||||||
|
}
|
||||||
|
}, 4000)
|
||||||
|
}
|
||||||
|
|
||||||
const openLink = async (url: string) => {
|
const openLink = async (url: string) => {
|
||||||
try {
|
try {
|
||||||
if (appStore.settings.app.open_links_in_browser) {
|
if (appStore.settings.app.open_links_in_browser) {
|
||||||
@@ -289,6 +391,8 @@ const openLink = async (url: string) => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await rcloneStore.checkRcloneBackendStatus()
|
await rcloneStore.checkRcloneBackendStatus()
|
||||||
statusCheckInterval = window.setInterval(rcloneStore.checkRcloneBackendStatus, 30 * 1000)
|
statusCheckInterval = window.setInterval(rcloneStore.checkRcloneBackendStatus, 30 * 1000)
|
||||||
|
|
||||||
|
await checkFirewallStatus()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -519,6 +623,41 @@ onUnmounted(() => {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.firewall-toggle-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid var(--color-border-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firewall-toggle-btn:hover:not(:disabled) {
|
||||||
|
background: var(--color-surface-elevated);
|
||||||
|
border-color: rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.firewall-toggle-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firewall-toggle-btn.active {
|
||||||
|
background: rgb(16, 185, 129);
|
||||||
|
color: white;
|
||||||
|
border-color: rgba(5, 150, 105, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.firewall-toggle-btn.active:hover:not(:disabled) {
|
||||||
|
background: rgb(5, 150, 105);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.actions-grid {
|
.actions-grid {
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
|
|||||||
@@ -32,7 +32,15 @@
|
|||||||
"stopRclone": "Stop RClone",
|
"stopRclone": "Stop RClone",
|
||||||
"manageMounts": "Manage Mounts",
|
"manageMounts": "Manage Mounts",
|
||||||
"autoLaunch": "Auto Launch Core(not app)",
|
"autoLaunch": "Auto Launch Core(not app)",
|
||||||
"showAdminPassword": "Show/Copy admin password from logs"
|
"showAdminPassword": "Show/Copy admin password from logs",
|
||||||
|
"firewall": {
|
||||||
|
"enable": "Allow Port",
|
||||||
|
"disable": "Remove Port Allow",
|
||||||
|
"added": "Port allowed successfully",
|
||||||
|
"removed": "Port removed successfully",
|
||||||
|
"failedToAdd": "Failed to allow port",
|
||||||
|
"failedToRemove": "Failed to remove port allow"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"coreMonitor": {
|
"coreMonitor": {
|
||||||
"title": "Core Monitor",
|
"title": "Core Monitor",
|
||||||
|
|||||||
@@ -32,7 +32,15 @@
|
|||||||
"stopRclone": "停止 RClone",
|
"stopRclone": "停止 RClone",
|
||||||
"manageMounts": "管理挂载",
|
"manageMounts": "管理挂载",
|
||||||
"autoLaunch": "自动启动核心(非桌面app)",
|
"autoLaunch": "自动启动核心(非桌面app)",
|
||||||
"showAdminPassword": "显示/复制日志中的管理员密码"
|
"showAdminPassword": "显示/复制日志中的管理员密码",
|
||||||
|
"firewall": {
|
||||||
|
"enable": "放行端口",
|
||||||
|
"disable": "移除端口放行",
|
||||||
|
"added": "端口放行成功",
|
||||||
|
"removed": "端口移除成功",
|
||||||
|
"failedToAdd": "添加端口放行失败",
|
||||||
|
"failedToRemove": "移除端口放行失败"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"coreMonitor": {
|
"coreMonitor": {
|
||||||
"title": "核心监控",
|
"title": "核心监控",
|
||||||
|
|||||||
@@ -453,7 +453,9 @@ const dismissWebdavTip = () => {
|
|||||||
localStorage.setItem('webdav_tip_dismissed', 'true')
|
localStorage.setItem('webdav_tip_dismissed', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWindows = /win/i.test(navigator.platform) || /win/i.test(navigator.userAgent)
|
const isWindows = computed(() => {
|
||||||
|
return typeof OS_PLATFORM !== 'undefined' && OS_PLATFORM === 'win32'
|
||||||
|
})
|
||||||
const showWinfspTip = ref(isWindows && !localStorage.getItem('winfsp_tip_dismissed'))
|
const showWinfspTip = ref(isWindows && !localStorage.getItem('winfsp_tip_dismissed'))
|
||||||
|
|
||||||
const dismissWinfspTip = () => {
|
const dismissWinfspTip = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user