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:
ysj7705andClaude Opus 5.5 committed 2026-10-24 21:58:32 +09:00
1 parent 027a029b47
commit 3c28c9dac2
9 files changed
+718 -27

No files matched your search

+70 -9
View File
@@ -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>
);
+16
View File
@@ -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;
}