// Wake‑on‑LAN (WoL) HTTP server with persistent storage and simple web UI. // // Usage: // // go run wol_server.go // // Environment variables: // // LISTEN – address to bind (default ":8080") // DB – JSON file for machine list (default "machines.json") // // Endpoints/API: // // GET /list – JSON map of machines (name ➜ MAC) // GET /wake/ – send WoL magic packet to // GET /manage – simple HTML UI with list/add/remove forms // POST /add (form/json) – add a machine ("name", "mac") // POST /remove (form/json) – remove a machine ("name") // // Security: for production, add authentication and restrict binding. package main import ( "crypto/rand" "encoding/hex" "encoding/json" "fmt" "html/template" "log" "net" "net/http" "os" "os/signal" "strconv" "strings" "sync" "syscall" "time" "golang.org/x/crypto/bcrypt" ) var ( username = GetENV("EW_USERNAME", "root") password = GetENV("EW_PASSWORD", "root") productive = Enabled("EW_PRODUCTIVE", false) hashedPassword = "" ) var ( machines map[string]string // name ➜ MAC mu sync.RWMutex // guards machines dbPath string // path to JSON file ) /*func init() { // Ensure correct MIME types for CSS and JS so browsers don't block them. _ = mime.AddExtensionType(".css", "text/css; charset=utf-8") _ = mime.AddExtensionType(".js", "application/javascript") }*/ // HTML template for the /manage UI. var manageTmpl = template.Must(template.New("manage").Parse(` WoL Manager

Wake-on-LAN Manager

Machines
{{range $name,$mac := .Machines}} {{end}}
Name MAC
Action
{{$name}} {{$mac}}
Add Machine
`)) func GetENV(k, d string) string { if v := os.Getenv(k); v != "" { return v } return d } func Enabled(k string, def bool) bool { b, err := strconv.ParseBool(strings.ToLower(os.Getenv(k))) if err != nil { return def } return b } var sessionStore = make(map[string]string) // token → username var loginAttempts = make(map[string]int) var loginLastAttempt = make(map[string]time.Time) var loginBlockedUntil = make(map[string]time.Time) var loginMutex sync.Mutex func isAuthenticated(r *http.Request) bool { cookie, err := r.Cookie("session") if err != nil { return false } // Prüfen, ob der Token im sessionStore existiert _, ok := sessionStore[cookie.Value] return ok } func hashPassword(pw string) string { hash, _ := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) return string(hash) } func checkPasswordHash(pw, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)) return err == nil } func generateSessionToken() string { b := make([]byte, 32) _, err := rand.Read(b) if err != nil { return "" // handle error besser im echten Code } return hex.EncodeToString(b) } func main() { // Determine DB path and load machines. hashedPassword = hashPassword(password) dbPath = GetENV("EW_DB", "/data/machines.json") loadMachines() // Save on SIGINT/SIGTERM. sig := make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt, syscall.SIGTERM) go func() { <-sig if err := saveMachines(); err != nil { log.Printf("error saving machines: %v", err) } os.Exit(0) }() // Register handlers. http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { ip := strings.Split(r.RemoteAddr, ":")[0] loginMutex.Lock() blockUntil, blocked := loginBlockedUntil[ip] if blocked && time.Now().Before(blockUntil) { loginMutex.Unlock() http.Error(w, "Zu viele Fehlversuche. Bitte versuch es später erneut.", http.StatusTooManyRequests) return } loginMutex.Unlock() if r.Method == http.MethodPost { r.ParseForm() user := r.FormValue("username") pass := r.FormValue("password") if user == username && checkPasswordHash(pass, hashedPassword) { token := generateSessionToken() // Speichere Session sessionStore[token] = user // Cookie setzen http.SetCookie(w, &http.Cookie{ Name: "session", Value: token, Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, }) // Erfolgreich -> Versuche zurücksetzen loginMutex.Lock() delete(loginAttempts, ip) delete(loginLastAttempt, ip) delete(loginBlockedUntil, ip) loginMutex.Unlock() http.Redirect(w, r, "/manage", http.StatusSeeOther) return } // Fehlversuch behandeln loginMutex.Lock() loginAttempts[ip]++ loginLastAttempt[ip] = time.Now() if loginAttempts[ip] >= 5 { loginBlockedUntil[ip] = time.Now().Add(10 * time.Minute) } loginMutex.Unlock() http.Error(w, "Login fehlgeschlagen", http.StatusUnauthorized) return } // GET: Login-Formular w.Header().Set("Content-Type", "text/html") w.Write([]byte(loginForm)) }) http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session") if err == nil { token := cookie.Value // Token aus dem serverseitigen Store löschen delete(sessionStore, token) // Cookie ungültig machen http.SetCookie(w, &http.Cookie{ Name: "session", Value: "", Path: "/", MaxAge: -1, HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, }) } http.Redirect(w, r, "/", http.StatusSeeOther) }) if productive { http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("/data/static")))) } else { http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) } http.HandleFunc("/list", listHandler) http.HandleFunc("/wake/", wakeHandler) http.HandleFunc("/wakeall", wakeAllHandler) http.HandleFunc("/manage", manageHandler) http.HandleFunc("/", manageHandler) http.HandleFunc("/add", addHandler) http.HandleFunc("/remove", removeHandler) addr := os.Getenv("LISTEN") if addr == "" { addr = ":8080" } log.Printf("WoL server listening on %s (DB: %s)", addr, dbPath) if err := http.ListenAndServe(addr, nil); err != nil { log.Fatalf("http server error: %v", err) } } // ========= persistence ========= func wakeAllHandler(w http.ResponseWriter, r *http.Request) { mu.RLock() defer mu.RUnlock() type StatusStruct struct { PCName string MAC string Status string } var ResultStruct []StatusStruct for a, b := range machines { err := sendMagicPacket(b) var c string if err == nil { c = "OK" } else { c = err.Error() } a1 := StatusStruct{PCName: a, MAC: b, Status: c} fmt.Println(a1) ResultStruct = append(ResultStruct, a1) } _ = json.NewEncoder(w).Encode(ResultStruct) } func loadMachines() { file, err := os.Open(dbPath) if err != nil { if os.IsNotExist(err) { machines = make(map[string]string) return } log.Fatalf("cannot open %s: %v", dbPath, err) } defer file.Close() if err := json.NewDecoder(file).Decode(&machines); err != nil { log.Fatalf("decode %s: %v", dbPath, err) } } func saveMachines() error { mu.RLock() defer mu.RUnlock() tmp := dbPath + ".tmp" f, err := os.Create(tmp) if err != nil { return err } if err := json.NewEncoder(f).Encode(machines); err != nil { f.Close() return err } f.Close() return os.Rename(tmp, dbPath) } // ========= handlers ========= // listHandler returns the current machines map as JSON. func listHandler(w http.ResponseWriter, r *http.Request) { mu.RLock() defer mu.RUnlock() w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(machines) } // wakeHandler sends a WoL magic packet to the requested machine name. func wakeHandler(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.Path, "/wake/") mu.RLock() mac, ok := machines[name] mu.RUnlock() if !ok { http.Error(w, "unknown machine", http.StatusNotFound) return } if err := sendMagicPacket(mac); err != nil { log.Printf("failed to wake %s: %v", name, err) http.Error(w, "failed to wake device", http.StatusInternalServerError) return } if r.Header.Get("Accept") == "text/html" { http.Redirect(w, r, "/manage", http.StatusSeeOther) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{"status": "ok", "host": name}) } // manageHandler serves a minimal HTML UI to list/add/remove machines. func manageHandler(w http.ResponseWriter, r *http.Request) { if !isAuthenticated(r) { http.Redirect(w, r, "/login", http.StatusSeeOther) return } mu.RLock() data := struct{ Machines map[string]string }{machines} mu.RUnlock() w.Header().Set("Content-Type", "text/html; charset=utf-8") _ = manageTmpl.Execute(w, data) } // addHandler adds a new machine via form or JSON. func addHandler(w http.ResponseWriter, r *http.Request) { if !isAuthenticated(r) { http.Redirect(w, r, "/login", http.StatusSeeOther) return } if err := r.ParseForm(); err != nil { http.Error(w, "invalid form", http.StatusBadRequest) return } name := strings.TrimSpace(r.FormValue("name")) mac := strings.TrimSpace(r.FormValue("mac")) if name == "" || mac == "" { http.Error(w, "name and mac required", http.StatusBadRequest) return } if _, err := net.ParseMAC(mac); err != nil { http.Error(w, "invalid mac", http.StatusBadRequest) return } mu.Lock() machines[name] = mac mu.Unlock() http.Redirect(w, r, "/manage", http.StatusSeeOther) } // removeHandler deletes a machine specified by name. func removeHandler(w http.ResponseWriter, r *http.Request) { if !isAuthenticated(r) { http.Redirect(w, r, "/login", http.StatusSeeOther) return } if err := r.ParseForm(); err != nil { http.Error(w, "invalid form", http.StatusBadRequest) return } name := strings.TrimSpace(r.FormValue("name")) if name == "" { http.Error(w, "name required", http.StatusBadRequest) return } mu.Lock() delete(machines, name) mu.Unlock() http.Redirect(w, r, "/manage", http.StatusSeeOther) } // ========= WoL core ========= // sendMagicPacket crafts and broadcasts the 102‑byte magic packet defined by the WoL spec. func sendMagicPacket(macAddr string) error { hwAddr, err := net.ParseMAC(macAddr) if err != nil { return fmt.Errorf("invalid MAC address %q: %w", macAddr, err) } // Build magic packet: 6×0xFF followed by MAC repeated 16 times. packet := make([]byte, 102) for i := 0; i < 6; i++ { packet[i] = 0xFF } for i := 0; i < 16; i++ { copy(packet[6+i*6:], hwAddr) } udpAddr := &net.UDPAddr{IP: net.IPv4bcast, Port: 9} conn, err := net.DialUDP("udp", nil, udpAddr) if err != nil { return fmt.Errorf("dial udp: %w", err) } defer conn.Close() if _, err := conn.Write(packet); err != nil { return fmt.Errorf("write packet: %w", err) } return nil } const loginForm = ` Login

Login

`