fix: harden Wake-on-LAN MAC handling

Normalize stored MAC addresses, avoid shell execution for ether-wake, handle missing WOL history gracefully, and improve the WOL menu display for named MAC entries.
This commit is contained in:
wenjie
2026-05-20 11:38:14 +08:00
parent 6ae742deed
commit 94749ea621
2 changed files with 208 additions and 95 deletions

View File

@@ -1,6 +1,7 @@
package network package network
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -33,8 +34,7 @@ func (s *Service) WakeOnLAN(c *gin.Context) {
return return
} }
command := fmt.Sprintf("ether-wake -b %s", mac) cmd := exec.Command("ether-wake", "-b", mac)
cmd := exec.Command("sh", "-c", command)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
@@ -43,7 +43,7 @@ func (s *Service) WakeOnLAN(c *gin.Context) {
return return
} }
go saveMac(mac) saveMac(mac)
rsp.OkRsp(c) rsp.OkRsp(c)
log.Debugf("wake on lan: %s", mac) log.Debugf("wake on lan: %s", mac)
@@ -52,14 +52,19 @@ func (s *Service) WakeOnLAN(c *gin.Context) {
func (s *Service) GetMac(c *gin.Context) { func (s *Service) GetMac(c *gin.Context) {
var rsp proto.Response var rsp proto.Response
content, err := os.ReadFile(WolMacFile) macs, err := readWolMacs()
if err != nil { if err != nil {
if errors.Is(err, os.ErrNotExist) {
rsp.OkRspWithData(c, &proto.GetMacRsp{Macs: []string{}})
return
}
rsp.ErrRsp(c, -2, "open file error") rsp.ErrRsp(c, -2, "open file error")
return return
} }
data := &proto.GetMacRsp{ data := &proto.GetMacRsp{
Macs: strings.Split(string(content), "\n"), Macs: macs,
} }
rsp.OkRspWithData(c, data) rsp.OkRspWithData(c, data)
@@ -74,36 +79,54 @@ func (s *Service) SetMacName(c *gin.Context) {
return return
} }
content, err := os.ReadFile(WolMacFile) mac, err := parseMAC(req.Mac)
if err != nil {
rsp.ErrRsp(c, -2, "invalid MAC address")
return
}
name := sanitizeWolMacName(req.Name)
if name == "" {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}
macs, err := readWolMacs()
if err != nil { if err != nil {
log.Errorf("failed to open %s: %s", WolMacFile, err) log.Errorf("failed to open %s: %s", WolMacFile, err)
rsp.ErrRsp(c, -2, "read failed") rsp.ErrRsp(c, -2, "read failed")
return return
} }
macs := strings.Split(string(content), "\n")
var newLines []string var newLines []string
macFound := false macFound := false
for _, line := range macs { for _, line := range macs {
parts := strings.Split(line, " ") itemMac, itemName, ok := splitWolMacLine(line)
if req.Mac != parts[0] { if !ok {
newLines = append(newLines, line)
continue continue
} }
newLines = append(newLines, parts[0]+" "+req.Name) normalizedMac, err := parseMAC(itemMac)
if err != nil {
newLines = append(newLines, formatWolMacLine(itemMac, itemName))
continue
}
if mac != normalizedMac {
newLines = append(newLines, formatWolMacLine(normalizedMac, itemName))
continue
}
newLines = append(newLines, formatWolMacLine(normalizedMac, name))
macFound = true macFound = true
} }
if !macFound { if !macFound {
log.Errorf("failed to found mac %s: %s", req.Mac, err) log.Errorf("failed to find mac %s", req.Mac)
rsp.ErrRsp(c, -3, "write failed") rsp.ErrRsp(c, -3, "write failed")
return return
} }
data := strings.Join(newLines, "\n") if err = writeWolMacs(newLines); err != nil {
err = os.WriteFile(WolMacFile, []byte(data), 0o644)
if err != nil {
log.Errorf("failed to write %s: %s", WolMacFile, err) log.Errorf("failed to write %s: %s", WolMacFile, err)
rsp.ErrRsp(c, -3, "write failed") rsp.ErrRsp(c, -3, "write failed")
return return
@@ -122,26 +145,37 @@ func (s *Service) DeleteMac(c *gin.Context) {
return return
} }
content, err := os.ReadFile(WolMacFile) mac, err := parseMAC(req.Mac)
if err != nil {
rsp.ErrRsp(c, -2, "invalid MAC address")
return
}
macs, err := readWolMacs()
if err != nil { if err != nil {
log.Errorf("failed to open %s: %s", WolMacFile, err) log.Errorf("failed to open %s: %s", WolMacFile, err)
rsp.ErrRsp(c, -2, "read failed") rsp.ErrRsp(c, -2, "read failed")
return return
} }
macs := strings.Split(string(content), "\n")
var newMacs []string var newMacs []string
for _, mac := range macs { for _, line := range macs {
parts := strings.Split(mac, " ") itemMac, itemName, ok := splitWolMacLine(line)
if req.Mac != parts[0] { if !ok {
newMacs = append(newMacs, mac) continue
}
normalizedMac, err := parseMAC(itemMac)
if err != nil {
newMacs = append(newMacs, formatWolMacLine(itemMac, itemName))
continue
}
if mac != normalizedMac {
newMacs = append(newMacs, formatWolMacLine(normalizedMac, itemName))
} }
} }
data := strings.Join(newMacs, "\n") if err = writeWolMacs(newMacs); err != nil {
err = os.WriteFile(WolMacFile, []byte(data), 0o644)
if err != nil {
log.Errorf("failed to write %s: %s", WolMacFile, err) log.Errorf("failed to write %s: %s", WolMacFile, err)
rsp.ErrRsp(c, -3, "write failed") rsp.ErrRsp(c, -3, "write failed")
return return
@@ -182,7 +216,7 @@ func saveMac(mac string) {
return return
} }
err := os.MkdirAll(filepath.Dir(WolMacFile), 0o644) err := os.MkdirAll(filepath.Dir(WolMacFile), 0o755)
if err != nil { if err != nil {
log.Errorf("failed to create dir: %s", err) log.Errorf("failed to create dir: %s", err)
return return
@@ -206,18 +240,85 @@ func saveMac(mac string) {
} }
func isMacExist(mac string) bool { func isMacExist(mac string) bool {
content, err := os.ReadFile(WolMacFile) macs, err := readWolMacs()
if err != nil { if err != nil {
return false return false
} }
macs := strings.Split(string(content), "\n")
for _, item := range macs { for _, item := range macs {
parts := strings.Split(item, " ") itemMac, _, ok := splitWolMacLine(item)
if mac == parts[0] { if !ok {
continue
}
normalizedMac, err := parseMAC(itemMac)
if err != nil {
continue
}
if mac == normalizedMac {
return true return true
} }
} }
return false return false
} }
func readWolMacs() ([]string, error) {
content, err := os.ReadFile(WolMacFile)
if err != nil {
return nil, err
}
lines := strings.Split(string(content), "\n")
macs := make([]string, 0, len(lines))
for _, line := range lines {
mac, name, ok := splitWolMacLine(line)
if !ok {
continue
}
macs = append(macs, formatWolMacLine(mac, name))
}
return macs, nil
}
func writeWolMacs(macs []string) error {
data := ""
if len(macs) > 0 {
data = strings.Join(macs, "\n") + "\n"
}
return os.WriteFile(WolMacFile, []byte(data), 0o644)
}
func splitWolMacLine(line string) (string, string, bool) {
line = strings.TrimSpace(line)
if line == "" {
return "", "", false
}
fields := strings.Fields(line)
if len(fields) == 0 {
return "", "", false
}
mac := fields[0]
if len(line) == len(mac) {
return mac, "", true
}
name := line[len(mac):]
return mac, strings.TrimSpace(name), true
}
func formatWolMacLine(mac string, name string) string {
if name == "" {
return mac
}
return mac + " " + name
}
func sanitizeWolMacName(name string) string {
return strings.Join(strings.Fields(name), " ")
}

View File

@@ -56,33 +56,37 @@ export const Wol = () => {
const isEdit = false; const isEdit = false;
const macList = rsp.data.macs const macList = rsp.data.macs
.filter((item: string) => item.trim() !== '') .map((item: string) => item.trim())
.filter((item: string) => item !== '')
.map((item: string) => { .map((item: string) => {
const parts = item.split(' '); const separator = item.search(/\s/);
const isName = parts.length > 1; const mac = separator === -1 ? item : item.slice(0, separator);
const name = isName ? parts[1] : ''; const name = separator === -1 ? '' : item.slice(separator).trim();
const mac = parts[0]; const isName = name !== '';
const isShow = !isName; const isShow = !isName;
return { name, mac, isShow, isName, isEdit }; return { name, mac, isShow, isName, isEdit };
}); });
setMacList([]);
setMacList(macList); setMacList(macList);
}); });
} }
function toggleShow(mac: string) { function toggleShow(mac: string) {
setMacList( setMacList((list) =>
macList.map((item) => (item.mac === mac ? { ...item, isShow: !item.isShow } : item)) list.map((item) => (item.mac === mac ? { ...item, isShow: !item.isShow } : item))
); );
} }
function editMac(mac: string, isEdit: boolean) { function editMac(mac: string, isEdit: boolean) {
setMacList(macList.map((item) => (item.mac === mac ? { ...item, isEdit: !isEdit } : item))); setMacList((list) =>
list.map((item) => (item.mac === mac ? { ...item, isEdit: !isEdit } : item))
);
} }
async function setMacName(e: React.KeyboardEvent<HTMLInputElement>, mac: string) { async function setMacName(e: React.KeyboardEvent<HTMLInputElement>, mac: string) {
const value: string = e.currentTarget.value; const value = e.currentTarget.value.trim();
if (!value) return;
const rsp = await setWolMacName(mac, value); const rsp = await setWolMacName(mac, value);
if (rsp.code !== 0) { if (rsp.code !== 0) {
console.log(rsp.msg); console.log(rsp.msg);
@@ -102,7 +106,7 @@ export const Wol = () => {
function wake(mac?: string) { function wake(mac?: string) {
if (status === 'loading') return; if (status === 'loading') return;
const value = mac ? mac : input; const value = (mac ? mac : input).trim();
if (!value) return; if (!value) return;
setStatus('loading'); setStatus('loading');
@@ -123,6 +127,7 @@ export const Wol = () => {
}) })
.catch(() => { .catch(() => {
setStatus('failed'); setStatus('failed');
setLog(t('auth.error'));
}); });
} }
@@ -134,15 +139,20 @@ export const Wol = () => {
<Divider style={{ margin: '10px 0 10px 0' }} /> <Divider style={{ margin: '10px 0 10px 0' }} />
<div className="pb-1 text-neutral-500">{t('wol.input')}</div> <div className="w-full space-y-1 py-3">
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<Input ref={inputRef} value={input} onChange={handleChange} onPressEnter={() => wake()} /> <Input
<Button type="primary" onClick={() => wake()}> ref={inputRef}
{t('wol.ok')} value={input}
</Button> placeholder={t('wol.input')}
</div> onChange={handleChange}
onPressEnter={() => wake()}
/>
<Button type="primary" onClick={() => wake()}>
{t('wol.ok')}
</Button>
</div>
<div className={clsx('py-2')}>
{status && ( {status && (
<div <div
className={clsx( className={clsx(
@@ -155,58 +165,60 @@ export const Wol = () => {
)} )}
</div> </div>
<List {macList.length > 0 && (
itemLayout="horizontal" <List
locale={{ emptyText: ' ' }} itemLayout="horizontal"
dataSource={macList} dataSource={macList}
renderItem={(item) => ( renderItem={(item) => (
<List.Item className="flex w-full items-center justify-between"> <List.Item className="flex w-full items-center justify-between">
<div className="h-[24px] max-w-[200px]"> <div className="h-[24px] max-w-[200px]">
{item.isEdit ? ( {item.isEdit ? (
<Input <Input
onFocus={() => setIsKeyboardEnable(false)} placeholder={item.mac}
onBlur={() => setIsKeyboardEnable(true)} onFocus={() => setIsKeyboardEnable(false)}
defaultValue={item.name} onBlur={() => setIsKeyboardEnable(true)}
onPressEnter={(e) => setMacName(e, item.mac)} defaultValue={item.name}
/> onPressEnter={(e) => setMacName(e, item.mac)}
) : item.isShow ? ( />
item.mac ) : item.isShow ? (
) : ( item.mac
item.name ) : (
)} item.name
</div> )}
</div>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
{item.isName && ( {item.isName && (
<div
className="text-500 flex h-[24px] w-[24px] cursor-pointer items-center justify-center rounded hover:bg-neutral-700/80"
onClick={() => toggleShow(item.mac)}
>
{item.isShow ? <EyeClosed size={16} /> : <Eye size={16} />}
</div>
)}
<div <div
className="text-500 flex h-[24px] w-[24px] cursor-pointer items-center justify-center rounded hover:bg-neutral-700/80" className="text-500 flex h-[24px] w-[24px] cursor-pointer items-center justify-center rounded hover:bg-neutral-700"
onClick={() => toggleShow(item.mac)} onClick={() => editMac(item.mac, item.isEdit)}
> >
{item.isShow ? <EyeClosed size={16} /> : <Eye size={16} />} <Pencil size={16} />
</div>
<div
className="flex h-[24px] w-[24px] cursor-pointer items-center justify-center rounded text-green-500 hover:bg-neutral-700/80"
onClick={() => wake(item.mac)}
>
<SendIcon size={16} />
</div>
<div
className="flex h-[24px] w-[24px] cursor-pointer items-center justify-center rounded text-red-500 hover:bg-neutral-700"
onClick={() => deleteMac(item.mac)}
>
<Trash2Icon size={16} />
</div> </div>
)}
<div
className="text-500 flex h-[24px] w-[24px] cursor-pointer items-center justify-center rounded hover:bg-neutral-700"
onClick={() => editMac(item.mac, item.isEdit)}
>
<Pencil size={16} />
</div> </div>
<div </List.Item>
className="flex h-[24px] w-[24px] cursor-pointer items-center justify-center rounded text-green-500 hover:bg-neutral-700/80" )}
onClick={() => wake(item.mac)} />
> )}
<SendIcon size={16} />
</div>
<div
className="flex h-[24px] w-[24px] cursor-pointer items-center justify-center rounded text-red-500 hover:bg-neutral-700"
onClick={() => deleteMac(item.mac)}
>
<Trash2Icon size={16} />
</div>
</div>
</List.Item>
)}
/>
</div> </div>
); );