-
No servers found. Please check your configuration.
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
\ No newline at end of file
+
+
diff --git a/main.go b/main.go
index 6570e47..4e47b30 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,7 @@
package main
import (
+ "flag"
"fmt"
"os"
@@ -11,48 +12,58 @@ import (
)
func main() {
+ debug := flag.Bool("debug", false, "Enable debug logging")
+ flag.Parse()
+
+ if *debug {
+ serveractions.Debug = true
+ serveractions.LogDebug("Debug mode enabled")
+ }
+
+ args := flag.Args()
+ if len(args) < 1 {
+ printUsage()
+ return
+ }
+
servers, err := loadConfig()
if err != nil {
fmt.Printf("Error loading servers: %v\n", err)
return
}
+ serveractions.LogDebug("Loaded %d servers from config", len(servers))
- if len(os.Args) < 2 {
- printUsage()
- return
- }
-
- command := os.Args[1]
+ command := args[0]
switch command {
case "list":
listServers(servers)
case "wake":
- if len(os.Args) < 3 {
+ if len(args) < 2 {
fmt.Println("Please specify a server name to wake.")
printUsage()
return
}
- serverName := os.Args[2]
+ serverName := args[1]
serveractions.WakeServer(serverName, servers)
case "wakeall":
serveractions.WakeAllServers(servers)
case "status":
serveractions.CheckServersStatus(servers)
case "shutdown":
- if len(os.Args) < 3 {
+ if len(args) < 2 {
fmt.Println("Please specify a server name to shutdown.")
printUsage()
return
}
- serverName := os.Args[2]
+ serverName := args[1]
serveractions.ShutdownServer(serverName, servers)
case "reboot":
- if len(os.Args) < 3 {
+ if len(args) < 2 {
fmt.Println("Please specify a server name to reboot.")
printUsage()
return
}
- serverName := os.Args[2]
+ serverName := args[1]
serveractions.RebootServer(serverName, servers)
case "serve":
webserver.StartWebServer(servers)
@@ -63,7 +74,9 @@ func main() {
}
func printUsage() {
- fmt.Println("Usage: go run .
")
+ fmt.Println("Usage: go run . [--debug] ")
+ fmt.Println("Options:")
+ fmt.Println(" --debug - Enable debug logging")
fmt.Println("Commands:")
fmt.Println(" list - List all configured servers")
fmt.Println(" wake - Wake a specific server")
@@ -75,14 +88,15 @@ func printUsage() {
}
func loadConfig() ([]serveractions.Server, error) {
- viper.SetConfigName("servers") // name of config file (without extension)
- viper.SetConfigType("json") // or viper.SetConfigType("YAML")
- viper.AddConfigPath("./config") // path to look for the config file in
- viper.AddConfigPath(".") // optionally look for config in the working directory
- err := viper.ReadInConfig() // Find and read the config file
- if err != nil { // Handle errors reading the config file
+ viper.SetConfigName("servers") // name of config file (without extension)
+ viper.SetConfigType("json") // or viper.SetConfigType("YAML")
+ viper.AddConfigPath("./config") // path to look for the config file in
+ viper.AddConfigPath(".") // optionally look for config in the working directory
+ err := viper.ReadInConfig() // Find and read the config file
+ if err != nil { // Handle errors reading the config file
// Check if the error is that the file doesn't exist
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
+ os.WriteFile("debug_config.log", []byte("Config file not found\n"), 0644)
// Config file not found; ignore error and return empty server list
return []serveractions.Server{}, nil
}
diff --git a/server-actions/server_actions.go b/server-actions/server_actions.go
index 221efd4..6a5d5d7 100644
--- a/server-actions/server_actions.go
+++ b/server-actions/server_actions.go
@@ -4,38 +4,78 @@ import (
"context"
"fmt"
"net"
+ "os"
"os/exec"
+ "regexp"
+ "strings"
"time"
"golang.org/x/crypto/ssh"
)
+var Debug bool
+
+func LogDebug(format string, v ...interface{}) {
+ if Debug {
+ fmt.Fprintf(os.Stderr, "[DEBUG] "+format+"\n", v...)
+ }
+}
+
func WakeServer(name string, servers []Server) {
for _, s := range servers {
if s.Name == name {
fmt.Printf("Sending Wake-on-LAN packet to %s (%s)...\n", s.Name, s.Mac)
- err := sendMagicPacket(s.Mac)
+ err := sendMagicPacket(s.Mac, s.IP)
if err != nil {
- fmt.Printf("Error sending packet: %v\n", err)
+ fmt.Printf("Error sending packet from local network: %v\n", err)
} else {
- fmt.Println("Packet sent successfully.")
+ fmt.Println("Local WOL packet sent successfully.")
}
+
+ // Try to wake via proxy if there's another online server in the same subnet
+ LogDebug("Checking for online servers in the same subnet to use as a proxy...")
+ for _, other := range servers {
+ if other.Name != s.Name && other.IP != "" && strings.HasPrefix(other.IP, s.IP[:strings.LastIndex(s.IP, ".")]) {
+ // Only use as proxy if we have credentials
+ if other.SSHUser != "" && other.SSHPass != "" {
+ if PingHost(other.IP) {
+ fmt.Printf("Attempting to wake %s via proxy %s...\n", s.Name, other.Name)
+ err := WakeServerRemote(other, s.Mac)
+ if err != nil {
+ LogDebug("Failed to wake via proxy %s: %v", other.Name, err)
+ } else {
+ fmt.Printf("Wake signal sent via proxy %s.\n", other.Name)
+ return
+ }
+ }
+ }
+ }
+ }
+
return
}
}
fmt.Printf("Server '%s' not found in configuration.\n", name)
}
+func WakeServerRemote(proxy Server, targetMac string) error {
+ // Send magic packet from the proxy server using a simple shell command
+ // We use python or similar if available, or just a simple bash command that constructs the packet
+ // constructing the packet in bash:
+ // echo -e $(printf 'f%.0s' {1..12}; printf "$(echo $MAC | sed 's/://g')%.0s" {1..16}) | xxd -r -p | nc -w1 -u -b 255.255.255.255 9
+
+ // A simpler way if wakeonlan is installed: wakeonlan $MAC
+ // But let's assume we might need to construct it or use a common tool.
+ // We'll try common tools first.
+ cmd := fmt.Sprintf("wakeonlan %s || ether-wake %s || (printf '%%b' \"$(printf '\\\\xff\\\\xff\\\\xff\\\\xff\\\\xff\\\\xff'; for i in {1..16}; do printf '%s' | sed 's/\\([0-9a-fA-F]\\{2\\}\\)/\\\\x\\1/g'; done)\" | nc -w1 -u -b 255.255.255.255 9)", targetMac, targetMac, targetMac)
+
+ return executeSSHCommand(proxy, cmd)
+}
+
func WakeAllServers(servers []Server) {
fmt.Println("Waking all servers...")
for _, s := range servers {
- fmt.Printf("Sending Wake-on-LAN packet to %s (%s)...\n", s.Name, s.Mac)
- err := sendMagicPacket(s.Mac)
- if err != nil {
- fmt.Printf(" - Error for %s: %v\n", s.Name, err)
- } else {
- fmt.Printf(" - Packet sent to %s.\n", s.Name)
- }
+ WakeServer(s.Name, servers)
}
}
@@ -56,17 +96,24 @@ func CheckServersStatus(servers []Server) {
}
func PingHost(host string) bool {
+ LogDebug("Pinging host: %s", host)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "ping", "-c", "1", "-W", "1", host)
err := cmd.Run()
+ if err != nil {
+ LogDebug("Ping failed for %s: %v", host, err)
+ } else {
+ LogDebug("Ping successful for %s", host)
+ }
return err == nil
}
// Creates and sends a magic packet to the specified MAC address.
-func sendMagicPacket(macAddr string) error {
+// Supports cross-VLAN by sending to both general and subnet-directed broadcast.
+func sendMagicPacket(macAddr string, ip string) error {
hwAddr, err := net.ParseMAC(macAddr)
if err != nil {
return fmt.Errorf("invalid mac address '%s': %w", macAddr, err)
@@ -80,14 +127,39 @@ func sendMagicPacket(macAddr string) error {
copy(magicPacket[i*6:], hwAddr)
}
- conn, err := net.Dial("udp", "255.255.255.255:9")
- if err != nil {
- return err
- }
- defer conn.Close()
+ // List of addresses to try sending the WOL packet to
+ // List of addresses to try sending the WOL packet to
+ broadcastAddresses := []string{"255.255.255.255"}
- _, err = conn.Write(magicPacket)
- return err
+ // If a valid IP is provided, calculate the directed broadcast address (assuming /24 subnet)
+ if ip != "" {
+ ipParts := strings.Split(ip, ".")
+ if len(ipParts) == 4 {
+ directedBroadcast := fmt.Sprintf("%s.%s.%s.255", ipParts[0], ipParts[1], ipParts[2])
+ if directedBroadcast != "255.255.255.255" {
+ broadcastAddresses = append(broadcastAddresses, directedBroadcast)
+ }
+ broadcastAddresses = append(broadcastAddresses, ip)
+ }
+ }
+
+ ports := []int{7, 9}
+ for _, addr := range broadcastAddresses {
+ for _, port := range ports {
+ fullAddr := fmt.Sprintf("%s:%d", addr, port)
+ LogDebug("Sending magic packet to %s", fullAddr)
+ for i := 0; i < 3; i++ {
+ conn, err := net.Dial("udp", fullAddr)
+ if err == nil {
+ conn.Write(magicPacket)
+ conn.Close()
+ }
+ time.Sleep(10 * time.Millisecond)
+ }
+ }
+ }
+
+ return nil
}
func ShutdownServer(name string, servers []Server) {
@@ -122,32 +194,149 @@ func RebootServer(name string, servers []Server) {
fmt.Printf("Server '%s' not found in configuration.\n", name)
}
-func executeSSHCommand(server Server, command string) error {
+func GetSSHClient(server Server) (*ssh.Client, error) {
config := &ssh.ClientConfig{
User: server.SSHUser,
Auth: []ssh.AuthMethod{
+ // Explicitly using ONLY Password and Keyboard-Interactive to avoid
+ // trying local SSH keys which might trigger "Too many attempts"
ssh.Password(server.SSHPass),
+ ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {
+ answers := make([]string, len(questions))
+ for i := range questions {
+ answers[i] = server.SSHPass
+ }
+ return answers, nil
+ }),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 10 * time.Second,
}
- client, err := ssh.Dial("tcp", server.IP+":22", config)
+ return ssh.Dial("tcp", server.IP+":22", config)
+}
+
+func executeSSHCommand(server Server, command string) error {
+ client, err := GetSSHClient(server)
if err != nil {
- return fmt.Errorf("failed to dial: %w", err)
+ return fmt.Errorf("failed to connect: %w", err)
}
defer client.Close()
+ LogDebug("SSH connection established to %s", server.IP)
+
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("failed to create session: %w", err)
}
defer session.Close()
- _, err = session.CombinedOutput(command)
+ LogDebug("Executing SSH command: %s", command)
+ output, err := session.CombinedOutput(command)
if err != nil {
+ LogDebug("SSH command failed: %v, Output: %s", err, string(output))
return fmt.Errorf("failed to run command: %w", err)
}
+ LogDebug("SSH command output: %s", string(output))
return nil
}
+func GetMacAddress(ip string) (string, error) {
+ // Ping the host to ensure it's in the ARP table
+ PingHost(ip)
+
+ LogDebug("Looking for MAC address for IP: %s", ip)
+ // Run arp -a to get the ARP table
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+
+ cmd := exec.CommandContext(ctx, "arp", "-a")
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ LogDebug("arp -a failed: %v", err)
+ return "", fmt.Errorf("error running arp: %v", err)
+ }
+
+ LogDebug("arp output received (%d bytes)", len(output))
+ // Regex to find the MAC address for the given IP
+ // Matches: (IP) at MAC or variations
+ lines := strings.Split(string(output), "\n")
+ for _, line := range lines {
+ if strings.Contains(line, "("+ip+")") || strings.Contains(line, " "+ip+" ") {
+ LogDebug("Found matching line in arp table: %s", line)
+ // Extract MAC address - usually looking for 6 pairs of hex digits separated by : or -
+ re := regexp.MustCompile(`([0-9a-fA-F]{1,2}[:\-]){5}[0-9a-fA-F]{1,2}`)
+ mac := re.FindString(line)
+ if mac != "" {
+ LogDebug("Extracted MAC: %s", mac)
+ return mac, nil
+ }
+ }
+ }
+
+ LogDebug("No MAC address found in arp table for IP %s", ip)
+ return "", fmt.Errorf("MAC address not found for IP %s", ip)
+}
+
+func GetMacAddressSSH(ip, user, pass string) (string, error) {
+ if user == "" || pass == "" {
+ return "", fmt.Errorf("SSH credentials required for remote MAC lookup")
+ }
+
+ LogDebug("Attempting to get MAC address via SSH for %s@%s", user, ip)
+
+ server := Server{
+ IP: ip,
+ SSHUser: user,
+ SSHPass: pass,
+ }
+
+ // Try common commands to get MAC on Linux/Unix
+ // ip link, ifconfig, etc.
+ cmd := "cat /sys/class/net/$(ip route get 8.8.8.8 | awk '{print $5}')/address 2>/dev/null || ifconfig | grep -E 'ether|HWaddr' | awk '{print $2}' | head -n 1"
+
+ config := &ssh.ClientConfig{
+ User: server.SSHUser,
+ Auth: []ssh.AuthMethod{
+ ssh.Password(server.SSHPass),
+ ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {
+ answers := make([]string, len(questions))
+ for i := range questions {
+ answers[i] = server.SSHPass
+ }
+ return answers, nil
+ }),
+ },
+ HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+ Timeout: 5 * time.Second,
+ }
+
+ client, err := ssh.Dial("tcp", server.IP+":22", config)
+ if err != nil {
+ return "", fmt.Errorf("SSH dial failed: %w", err)
+ }
+ defer client.Close()
+
+ session, err := client.NewSession()
+ if err != nil {
+ return "", fmt.Errorf("failed to create session: %w", err)
+ }
+ defer session.Close()
+
+ output, err := session.CombinedOutput(cmd)
+ if err != nil {
+ return "", fmt.Errorf("SSH command failed: %w", err)
+ }
+
+ mac := strings.TrimSpace(string(output))
+ // Validate MAC format
+ re := regexp.MustCompile(`([0-9a-fA-F]{1,2}[:\-]){5}[0-9a-fA-F]{1,2}`)
+ mac = re.FindString(mac)
+
+ if mac == "" {
+ return "", fmt.Errorf("could not extract MAC address from SSH output")
+ }
+
+ LogDebug("Successfully retrieved MAC via SSH: %s", mac)
+ return mac, nil
+}
diff --git a/server-actions/types.go b/server-actions/types.go
index 3fdac67..eb98ffb 100644
--- a/server-actions/types.go
+++ b/server-actions/types.go
@@ -2,9 +2,9 @@ package serveractions
// Server struct to hold server information
type Server struct {
- Name string `json:"name"`
- Mac string `json:"mac"`
- IP string `json:"ip"`
- SSHUser string `json:"ssh_user"`
- SSHPass string `json:"ssh_pass"`
+ Name string `json:"name" mapstructure:"name"`
+ Mac string `json:"mac" mapstructure:"mac"`
+ IP string `json:"ip" mapstructure:"ip"`
+ SSHUser string `json:"ssh_user" mapstructure:"ssh_user"`
+ SSHPass string `json:"ssh_pass" mapstructure:"ssh_pass"`
}
diff --git a/webserver/terminal.go b/webserver/terminal.go
new file mode 100644
index 0000000..8a77be0
--- /dev/null
+++ b/webserver/terminal.go
@@ -0,0 +1,153 @@
+package webserver
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+
+ serveractions "manage-servers/server-actions"
+
+ "github.com/gorilla/websocket"
+ "golang.org/x/crypto/ssh"
+)
+
+var upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ CheckOrigin: func(r *http.Request) bool {
+ return true // Allowing all origins for local tool
+ },
+}
+
+func (ws *WebServer) handleSSH(w http.ResponseWriter, r *http.Request) {
+ serverName := r.URL.Query().Get("name")
+ if serverName == "" {
+ http.Error(w, "Missing server name", http.StatusBadRequest)
+ return
+ }
+
+ var targetServer *serveractions.Server
+ for _, s := range ws.servers {
+ if s.Name == serverName {
+ targetServer = &s
+ break
+ }
+ }
+
+ if targetServer == nil {
+ http.Error(w, "Server not found", http.StatusNotFound)
+ return
+ }
+
+ if targetServer.SSHUser == "" || targetServer.SSHPass == "" {
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err == nil {
+ conn.WriteMessage(websocket.TextMessage, []byte("\r\n[Error] SSH Credentials missing for this node.\r\nPlease edit the server entry and save with a Username and Password.\r\n"))
+ conn.Close()
+ }
+ return
+ }
+
+ conn, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Printf("Websocket upgrade failed: %v", err)
+ return
+ }
+ defer conn.Close()
+
+ client, err := serveractions.GetSSHClient(*targetServer)
+ if err != nil {
+ conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("\r\n[Error] SSH Connection failed: %v\r\n", err)))
+ return
+ }
+ defer client.Close()
+
+ session, err := client.NewSession()
+ if err != nil {
+ conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("\r\n[Error] SSH Session failed: %v\r\n", err)))
+ return
+ }
+ defer session.Close()
+
+ // Request pseudo-terminal
+ modes := ssh.TerminalModes{
+ ssh.ECHO: 1,
+ ssh.TTY_OP_ISPEED: 14400,
+ ssh.TTY_OP_OSPEED: 14400,
+ }
+
+ // Default to 24x80 if not provided
+ rows := 24
+ cols := 80
+ if r.URL.Query().Get("rows") != "" {
+ fmt.Sscanf(r.URL.Query().Get("rows"), "%d", &rows)
+ }
+ if r.URL.Query().Get("cols") != "" {
+ fmt.Sscanf(r.URL.Query().Get("cols"), "%d", &cols)
+ }
+
+ if err := session.RequestPty("xterm-256color", rows, cols, modes); err != nil {
+ conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("\r\n[Error] Request for PTY failed: %v\r\n", err)))
+ return
+ }
+
+ stdin, err := session.StdinPipe()
+ if err != nil {
+ return
+ }
+ stdout, err := session.StdoutPipe()
+ if err != nil {
+ return
+ }
+ stderr, err := session.StderrPipe()
+ if err != nil {
+ return
+ }
+
+ if err := session.Shell(); err != nil {
+ conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("\r\n[Error] Shell start failed: %v\r\n", err)))
+ return
+ }
+
+ // Proxy stdout/stderr to WebSocket
+ go func() {
+ buf := make([]byte, 1024)
+ for {
+ n, err := stdout.Read(buf)
+ if n > 0 {
+ if err := conn.WriteMessage(websocket.TextMessage, buf[:n]); err != nil {
+ return
+ }
+ }
+ if err != nil {
+ return
+ }
+ }
+ }()
+
+ go func() {
+ buf := make([]byte, 1024)
+ for {
+ n, err := stderr.Read(buf)
+ if n > 0 {
+ if err := conn.WriteMessage(websocket.TextMessage, buf[:n]); err != nil {
+ return
+ }
+ }
+ if err != nil {
+ return
+ }
+ }
+ }()
+
+ // Proxy WebSocket to stdin
+ for {
+ _, message, err := conn.ReadMessage()
+ if err != nil {
+ break
+ }
+ if _, err := stdin.Write(message); err != nil {
+ break
+ }
+ }
+}
diff --git a/webserver/webserver.go b/webserver/webserver.go
index b9ea9ce..5d40cd8 100644
--- a/webserver/webserver.go
+++ b/webserver/webserver.go
@@ -24,6 +24,8 @@ func StartWebServer(servers []serveractions.Server) {
http.HandleFunc("/shutdown", ws.handleShutdownServer)
http.HandleFunc("/reboot", ws.handleRebootServer)
http.HandleFunc("/status", ws.handleStatus)
+ http.HandleFunc("/find-mac", ws.handleFindMac)
+ http.HandleFunc("/ssh", ws.handleSSH)
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Printf("Error starting server: %v\n", err)
@@ -31,15 +33,19 @@ func StartWebServer(servers []serveractions.Server) {
}
func (ws *WebServer) handleRoot(w http.ResponseWriter, r *http.Request) {
+ serveractions.LogDebug("Handling root request: %s %s", r.Method, r.URL.Path)
http.ServeFile(w, r, "index.html")
}
func (ws *WebServer) handleServers(w http.ResponseWriter, r *http.Request) {
+ serveractions.LogDebug("Handling /servers request: %s", r.Method)
switch r.Method {
case http.MethodGet:
ws.getServers(w, r)
case http.MethodPost:
ws.addServer(w, r)
+ case http.MethodPut:
+ ws.updateServer(w, r)
case http.MethodDelete:
ws.deleteServer(w, r)
default:
@@ -52,6 +58,42 @@ func (ws *WebServer) getServers(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(ws.servers)
}
+func (ws *WebServer) updateServer(w http.ResponseWriter, r *http.Request) {
+ oldName := r.URL.Query().Get("oldName")
+ if oldName == "" {
+ http.Error(w, "Missing old server name", http.StatusBadRequest)
+ return
+ }
+
+ var updatedServer serveractions.Server
+ if err := json.NewDecoder(r.Body).Decode(&updatedServer); err != nil {
+ http.Error(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ found := false
+ for i, server := range ws.servers {
+ if server.Name == oldName {
+ ws.servers[i] = updatedServer
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ http.Error(w, "Server not found", http.StatusNotFound)
+ return
+ }
+
+ viper.Set("servers", ws.servers)
+ if err := viper.WriteConfig(); err != nil {
+ http.Error(w, fmt.Sprintf("Error writing config file: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+}
+
func (ws *WebServer) addServer(w http.ResponseWriter, r *http.Request) {
var newServer serveractions.Server
if err := json.NewDecoder(r.Body).Decode(&newServer); err != nil {
@@ -143,6 +185,7 @@ func (ws *WebServer) handleRebootServer(w http.ResponseWriter, r *http.Request)
func (ws *WebServer) handleStatus(w http.ResponseWriter, r *http.Request) {
serverName := r.URL.Query().Get("name")
+ serveractions.LogDebug("Checking status for server: %s", serverName)
if serverName == "" {
http.Error(w, "Missing server name", http.StatusBadRequest)
return
@@ -162,3 +205,36 @@ func (ws *WebServer) handleStatus(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Server not found", http.StatusNotFound)
}
+
+func (ws *WebServer) handleFindMac(w http.ResponseWriter, r *http.Request) {
+ ip := r.URL.Query().Get("ip")
+ user := r.URL.Query().Get("user")
+ pass := r.URL.Query().Get("pass")
+
+ serveractions.LogDebug("Handling /find-mac request for IP: %s (SSH provided: %v)", ip, user != "")
+ if ip == "" {
+ http.Error(w, "Missing IP address", http.StatusBadRequest)
+ return
+ }
+
+ // Try ARP first
+ mac, err := serveractions.GetMacAddress(ip)
+ if err != nil {
+ serveractions.LogDebug("ARP lookup failed for %s: %v. Trying SSH fallback...", ip, err)
+ // Try SSH fallback if credentials are provided
+ if user != "" && pass != "" {
+ mac, err = serveractions.GetMacAddressSSH(ip, user, pass)
+ if err != nil {
+ serveractions.LogDebug("SSH lookup also failed for %s: %v", ip, err)
+ http.Error(w, "MAC address not found via ARP or SSH", http.StatusNotFound)
+ return
+ }
+ } else {
+ serveractions.LogDebug("No SSH credentials for fallback for %s", ip)
+ http.Error(w, "MAC address not found in local network. Please provide SSH credentials for remote lookup.", http.StatusNotFound)
+ return
+ }
+ }
+
+ w.Write([]byte(mac))
+}