mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
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:
@@ -1,6 +1,7 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -33,8 +34,7 @@ func (s *Service) WakeOnLAN(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("ether-wake -b %s", mac)
|
||||
cmd := exec.Command("sh", "-c", command)
|
||||
cmd := exec.Command("ether-wake", "-b", mac)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -43,7 +43,7 @@ func (s *Service) WakeOnLAN(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
go saveMac(mac)
|
||||
saveMac(mac)
|
||||
|
||||
rsp.OkRsp(c)
|
||||
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) {
|
||||
var rsp proto.Response
|
||||
|
||||
content, err := os.ReadFile(WolMacFile)
|
||||
macs, err := readWolMacs()
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
rsp.OkRspWithData(c, &proto.GetMacRsp{Macs: []string{}})
|
||||
return
|
||||
}
|
||||
|
||||
rsp.ErrRsp(c, -2, "open file error")
|
||||
return
|
||||
}
|
||||
|
||||
data := &proto.GetMacRsp{
|
||||
Macs: strings.Split(string(content), "\n"),
|
||||
Macs: macs,
|
||||
}
|
||||
|
||||
rsp.OkRspWithData(c, data)
|
||||
@@ -74,36 +79,54 @@ func (s *Service) SetMacName(c *gin.Context) {
|
||||
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 {
|
||||
log.Errorf("failed to open %s: %s", WolMacFile, err)
|
||||
rsp.ErrRsp(c, -2, "read failed")
|
||||
return
|
||||
}
|
||||
|
||||
macs := strings.Split(string(content), "\n")
|
||||
var newLines []string
|
||||
macFound := false
|
||||
|
||||
for _, line := range macs {
|
||||
parts := strings.Split(line, " ")
|
||||
if req.Mac != parts[0] {
|
||||
newLines = append(newLines, line)
|
||||
itemMac, itemName, ok := splitWolMacLine(line)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
data := strings.Join(newLines, "\n")
|
||||
err = os.WriteFile(WolMacFile, []byte(data), 0o644)
|
||||
if err != nil {
|
||||
if err = writeWolMacs(newLines); err != nil {
|
||||
log.Errorf("failed to write %s: %s", WolMacFile, err)
|
||||
rsp.ErrRsp(c, -3, "write failed")
|
||||
return
|
||||
@@ -122,26 +145,37 @@ func (s *Service) DeleteMac(c *gin.Context) {
|
||||
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 {
|
||||
log.Errorf("failed to open %s: %s", WolMacFile, err)
|
||||
rsp.ErrRsp(c, -2, "read failed")
|
||||
return
|
||||
}
|
||||
|
||||
macs := strings.Split(string(content), "\n")
|
||||
var newMacs []string
|
||||
|
||||
for _, mac := range macs {
|
||||
parts := strings.Split(mac, " ")
|
||||
if req.Mac != parts[0] {
|
||||
newMacs = append(newMacs, mac)
|
||||
for _, line := range macs {
|
||||
itemMac, itemName, ok := splitWolMacLine(line)
|
||||
if !ok {
|
||||
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")
|
||||
err = os.WriteFile(WolMacFile, []byte(data), 0o644)
|
||||
if err != nil {
|
||||
if err = writeWolMacs(newMacs); err != nil {
|
||||
log.Errorf("failed to write %s: %s", WolMacFile, err)
|
||||
rsp.ErrRsp(c, -3, "write failed")
|
||||
return
|
||||
@@ -182,7 +216,7 @@ func saveMac(mac string) {
|
||||
return
|
||||
}
|
||||
|
||||
err := os.MkdirAll(filepath.Dir(WolMacFile), 0o644)
|
||||
err := os.MkdirAll(filepath.Dir(WolMacFile), 0o755)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create dir: %s", err)
|
||||
return
|
||||
@@ -206,18 +240,85 @@ func saveMac(mac string) {
|
||||
}
|
||||
|
||||
func isMacExist(mac string) bool {
|
||||
content, err := os.ReadFile(WolMacFile)
|
||||
macs, err := readWolMacs()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
macs := strings.Split(string(content), "\n")
|
||||
for _, item := range macs {
|
||||
parts := strings.Split(item, " ")
|
||||
if mac == parts[0] {
|
||||
itemMac, _, ok := splitWolMacLine(item)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
normalizedMac, err := parseMAC(itemMac)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if mac == normalizedMac {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
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), " ")
|
||||
}
|
||||
|
||||
@@ -56,33 +56,37 @@ export const Wol = () => {
|
||||
|
||||
const isEdit = false;
|
||||
const macList = rsp.data.macs
|
||||
.filter((item: string) => item.trim() !== '')
|
||||
.map((item: string) => item.trim())
|
||||
.filter((item: string) => item !== '')
|
||||
.map((item: string) => {
|
||||
const parts = item.split(' ');
|
||||
const isName = parts.length > 1;
|
||||
const name = isName ? parts[1] : '';
|
||||
const mac = parts[0];
|
||||
const separator = item.search(/\s/);
|
||||
const mac = separator === -1 ? item : item.slice(0, separator);
|
||||
const name = separator === -1 ? '' : item.slice(separator).trim();
|
||||
const isName = name !== '';
|
||||
const isShow = !isName;
|
||||
return { name, mac, isShow, isName, isEdit };
|
||||
});
|
||||
|
||||
setMacList([]);
|
||||
setMacList(macList);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleShow(mac: string) {
|
||||
setMacList(
|
||||
macList.map((item) => (item.mac === mac ? { ...item, isShow: !item.isShow } : item))
|
||||
setMacList((list) =>
|
||||
list.map((item) => (item.mac === mac ? { ...item, isShow: !item.isShow } : item))
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
const value: string = e.currentTarget.value;
|
||||
const value = e.currentTarget.value.trim();
|
||||
if (!value) return;
|
||||
|
||||
const rsp = await setWolMacName(mac, value);
|
||||
if (rsp.code !== 0) {
|
||||
console.log(rsp.msg);
|
||||
@@ -102,7 +106,7 @@ export const Wol = () => {
|
||||
function wake(mac?: string) {
|
||||
if (status === 'loading') return;
|
||||
|
||||
const value = mac ? mac : input;
|
||||
const value = (mac ? mac : input).trim();
|
||||
if (!value) return;
|
||||
|
||||
setStatus('loading');
|
||||
@@ -123,6 +127,7 @@ export const Wol = () => {
|
||||
})
|
||||
.catch(() => {
|
||||
setStatus('failed');
|
||||
setLog(t('auth.error'));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,15 +139,20 @@ export const Wol = () => {
|
||||
|
||||
<Divider style={{ margin: '10px 0 10px 0' }} />
|
||||
|
||||
<div className="pb-1 text-neutral-500">{t('wol.input')}</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Input ref={inputRef} value={input} onChange={handleChange} onPressEnter={() => wake()} />
|
||||
<Button type="primary" onClick={() => wake()}>
|
||||
{t('wol.ok')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full space-y-1 py-3">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
placeholder={t('wol.input')}
|
||||
onChange={handleChange}
|
||||
onPressEnter={() => wake()}
|
||||
/>
|
||||
<Button type="primary" onClick={() => wake()}>
|
||||
{t('wol.ok')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={clsx('py-2')}>
|
||||
{status && (
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -155,58 +165,60 @@ export const Wol = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
locale={{ emptyText: ' ' }}
|
||||
dataSource={macList}
|
||||
renderItem={(item) => (
|
||||
<List.Item className="flex w-full items-center justify-between">
|
||||
<div className="h-[24px] max-w-[200px]">
|
||||
{item.isEdit ? (
|
||||
<Input
|
||||
onFocus={() => setIsKeyboardEnable(false)}
|
||||
onBlur={() => setIsKeyboardEnable(true)}
|
||||
defaultValue={item.name}
|
||||
onPressEnter={(e) => setMacName(e, item.mac)}
|
||||
/>
|
||||
) : item.isShow ? (
|
||||
item.mac
|
||||
) : (
|
||||
item.name
|
||||
)}
|
||||
</div>
|
||||
{macList.length > 0 && (
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={macList}
|
||||
renderItem={(item) => (
|
||||
<List.Item className="flex w-full items-center justify-between">
|
||||
<div className="h-[24px] max-w-[200px]">
|
||||
{item.isEdit ? (
|
||||
<Input
|
||||
placeholder={item.mac}
|
||||
onFocus={() => setIsKeyboardEnable(false)}
|
||||
onBlur={() => setIsKeyboardEnable(true)}
|
||||
defaultValue={item.name}
|
||||
onPressEnter={(e) => setMacName(e, item.mac)}
|
||||
/>
|
||||
) : item.isShow ? (
|
||||
item.mac
|
||||
) : (
|
||||
item.name
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
{item.isName && (
|
||||
<div className="flex items-center space-x-1">
|
||||
{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
|
||||
className="text-500 flex h-[24px] w-[24px] cursor-pointer items-center justify-center rounded hover:bg-neutral-700/80"
|
||||
onClick={() => toggleShow(item.mac)}
|
||||
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)}
|
||||
>
|
||||
{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
|
||||
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
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user