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
|
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), " ")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user