feat: install, update and remove Mudfish from the panel
- Install: download the latest package from mudfish.net, verify the Makeself MD5, unpack the bzip2 tar ourselves (no installer script is executed; path traversal and links rejected) into /opt/mudfish/<ver>. - Update: checked at most daily; button-only; refused while the tunnel is up; carries the saved login (.conf, .conf.db) into the new version, restarts the daemon and keeps only the previous version. - Remove: button with confirmation, and automatically when the plugin is uninstalled: VPN off, daemon stopped, mudfish-tap removed, /opt/mudfish deleted, plugin settings cleared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
027a029b47
commit
3c28c9dac2
9 files changed
+718
-27
No files matched your search
+70
-9
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ButtonItem,
|
||||
ConfirmModal,
|
||||
DropdownItem,
|
||||
Field,
|
||||
PanelSection,
|
||||
@@ -13,7 +14,11 @@ import {
|
||||
connect,
|
||||
disconnect,
|
||||
getState,
|
||||
installMudfish,
|
||||
isNewer,
|
||||
listNodes,
|
||||
uninstallMudfish,
|
||||
updateMudfish,
|
||||
NodeInfo,
|
||||
Phase,
|
||||
setNode,
|
||||
@@ -134,6 +139,42 @@ export function Panel() {
|
||||
...nodes.map((n) => ({ data: String(n.sid), label: `${n.label} · ${rttText(n.rtt)}` })),
|
||||
];
|
||||
const selected = state.node_mode === "auto" ? "auto" : String(state.node?.sid ?? "auto");
|
||||
const version = state.mudfish?.version ?? null;
|
||||
const latest = state.mudfish?.latest ?? null;
|
||||
const missing = state.phase === "missing";
|
||||
|
||||
const confirmUninstall = () =>
|
||||
showModal(
|
||||
<ConfirmModal
|
||||
strTitle="미꾸라지 제거"
|
||||
strDescription="VPN을 끄고 미꾸라지(/opt/mudfish 전체, 로그인 정보 포함)와 이 플러그인의 설정을 지워요. 미꾸라지 계정은 그대로예요."
|
||||
strOKButtonText="제거"
|
||||
strCancelButtonText="취소"
|
||||
onOK={() => run(uninstallMudfish)}
|
||||
/>,
|
||||
);
|
||||
|
||||
if (missing) {
|
||||
return (
|
||||
<PanelSection>
|
||||
<PanelSectionRow>
|
||||
<Field label="상태" focusable={false}>
|
||||
<StatusValue state={state} />
|
||||
</Field>
|
||||
</PanelSectionRow>
|
||||
{state.message && (
|
||||
<PanelSectionRow>
|
||||
<Field focusable={false} description={state.message} />
|
||||
</PanelSectionRow>
|
||||
)}
|
||||
<PanelSectionRow>
|
||||
<ButtonItem layout="below" disabled={busy} onClick={() => run(installMudfish)}>
|
||||
{busy ? "설치 중… (1~2분)" : `미꾸라지 설치${latest ? ` (${latest})` : ""}`}
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
</PanelSection>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelSection>
|
||||
@@ -173,15 +214,35 @@ export function Panel() {
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
{advanced && (
|
||||
<PanelSectionRow>
|
||||
<DropdownItem
|
||||
label="일본 노드"
|
||||
rgOptions={options}
|
||||
selectedOption={selected}
|
||||
disabled={busy || state.phase === "connecting"}
|
||||
onChange={(opt) => run(() => setNode(String(opt.data)))}
|
||||
/>
|
||||
</PanelSectionRow>
|
||||
<>
|
||||
<PanelSectionRow>
|
||||
<DropdownItem
|
||||
label="일본 노드"
|
||||
rgOptions={options}
|
||||
selectedOption={selected}
|
||||
disabled={busy || state.phase === "connecting"}
|
||||
onChange={(opt) => run(() => setNode(String(opt.data)))}
|
||||
/>
|
||||
</PanelSectionRow>
|
||||
<PanelSectionRow>
|
||||
<Field label="미꾸라지" focusable={false}>
|
||||
{version ?? "?"}
|
||||
{latest && !isNewer(latest, version) ? " (최신)" : ""}
|
||||
</Field>
|
||||
</PanelSectionRow>
|
||||
{isNewer(latest, version) && (
|
||||
<PanelSectionRow>
|
||||
<ButtonItem layout="below" disabled={busy || active} onClick={() => run(updateMudfish)}>
|
||||
{busy ? "업데이트 중…" : `미꾸라지 업데이트 (${version} → ${latest})`}
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
)}
|
||||
<PanelSectionRow>
|
||||
<ButtonItem layout="below" disabled={busy} onClick={confirmUninstall}>
|
||||
미꾸라지 제거
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
</>
|
||||
)}
|
||||
</PanelSection>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface VpnState {
|
||||
tunnel: boolean;
|
||||
need_login: boolean;
|
||||
login: { url: string; expires_at: number } | null;
|
||||
mudfish: { version: string | null; latest: string | null } | null;
|
||||
}
|
||||
|
||||
export const getState = callable<[], VpnState>("get_state");
|
||||
@@ -28,3 +29,18 @@ export const setNode = callable<[choice: string], VpnState>("set_node");
|
||||
export const listNodes = callable<[], NodeInfo[]>("list_nodes");
|
||||
export const startLogin = callable<[], VpnState>("start_login");
|
||||
export const stopLogin = callable<[], VpnState>("stop_login");
|
||||
export const installMudfish = callable<[], VpnState>("install_mudfish");
|
||||
export const updateMudfish = callable<[], VpnState>("update_mudfish");
|
||||
export const uninstallMudfish = callable<[], VpnState>("uninstall_mudfish");
|
||||
|
||||
/** True when version `a` is newer than `b` (dotted numeric versions). */
|
||||
export function isNewer(a: string | null | undefined, b: string | null | undefined): boolean {
|
||||
if (!a || !b) return false;
|
||||
const pa = a.split(".").map(Number);
|
||||
const pb = b.split(".").map(Number);
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
||||
if (d !== 0) return d > 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in new issue
Block a user