mirror of
https://github.com/OpenListTeam/OpenList-Desktop.git
synced 2025-11-25 03:14:56 +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",
|
||||
"fix:rust": "cd src-tauri && cargo fmt --all && cargo clippy --all-targets --all-features --fix --allow-dirty",
|
||||
"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": {
|
||||
"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 config;
|
||||
pub mod custom_updater;
|
||||
pub mod firewall;
|
||||
pub mod http_api;
|
||||
pub mod logs;
|
||||
pub mod openlist_core;
|
||||
|
||||
@@ -13,6 +13,7 @@ use cmd::custom_updater::{
|
||||
check_for_updates, download_update, get_current_version, install_update_and_restart,
|
||||
is_auto_check_enabled, set_auto_check_enabled,
|
||||
};
|
||||
use cmd::firewall::{add_firewall_rule, check_firewall_rule, remove_firewall_rule};
|
||||
use cmd::http_api::{
|
||||
delete_process, get_process_list, restart_process, start_process, stop_process, update_process,
|
||||
};
|
||||
@@ -159,6 +160,9 @@ pub fn run() {
|
||||
check_service_status,
|
||||
stop_service,
|
||||
start_service,
|
||||
check_firewall_rule,
|
||||
add_firewall_rule,
|
||||
remove_firewall_rule,
|
||||
check_for_updates,
|
||||
download_update,
|
||||
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))
|
||||
}
|
||||
|
||||
// --- 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 ---
|
||||
static updater = {
|
||||
check: (): Promise<UpdateCheck> => call('check_for_updates'),
|
||||
|
||||
@@ -75,6 +75,28 @@
|
||||
<input type="checkbox" v-model="settings.openlist.auto_launch" @change="handleAutoLaunchToggle" />
|
||||
<span class="toggle-text">{{ t('dashboard.quickActions.autoLaunch') }}</span>
|
||||
</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>
|
||||
@@ -82,13 +104,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '../../stores/app'
|
||||
import { useRcloneStore } from '../../stores/rclone'
|
||||
import { useTranslation } from '../../composables/useI18n'
|
||||
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'
|
||||
|
||||
const { t } = useTranslation()
|
||||
@@ -100,6 +122,12 @@ const isCoreRunning = computed(() => appStore.isCoreRunning)
|
||||
const settings = computed(() => appStore.settings)
|
||||
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(() => {
|
||||
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) => {
|
||||
try {
|
||||
if (appStore.settings.app.open_links_in_browser) {
|
||||
@@ -289,6 +391,8 @@ const openLink = async (url: string) => {
|
||||
onMounted(async () => {
|
||||
await rcloneStore.checkRcloneBackendStatus()
|
||||
statusCheckInterval = window.setInterval(rcloneStore.checkRcloneBackendStatus, 30 * 1000)
|
||||
|
||||
await checkFirewallStatus()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -519,6 +623,41 @@ onUnmounted(() => {
|
||||
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) {
|
||||
.actions-grid {
|
||||
gap: 1.5rem;
|
||||
|
||||
@@ -32,7 +32,15 @@
|
||||
"stopRclone": "Stop RClone",
|
||||
"manageMounts": "Manage Mounts",
|
||||
"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": {
|
||||
"title": "Core Monitor",
|
||||
|
||||
@@ -32,7 +32,15 @@
|
||||
"stopRclone": "停止 RClone",
|
||||
"manageMounts": "管理挂载",
|
||||
"autoLaunch": "自动启动核心(非桌面app)",
|
||||
"showAdminPassword": "显示/复制日志中的管理员密码"
|
||||
"showAdminPassword": "显示/复制日志中的管理员密码",
|
||||
"firewall": {
|
||||
"enable": "放行端口",
|
||||
"disable": "移除端口放行",
|
||||
"added": "端口放行成功",
|
||||
"removed": "端口移除成功",
|
||||
"failedToAdd": "添加端口放行失败",
|
||||
"failedToRemove": "移除端口放行失败"
|
||||
}
|
||||
},
|
||||
"coreMonitor": {
|
||||
"title": "核心监控",
|
||||
|
||||
@@ -453,7 +453,9 @@ const dismissWebdavTip = () => {
|
||||
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 dismissWinfspTip = () => {
|
||||
|
||||
Reference in New Issue
Block a user