From 84d396cfb6c118b911c202420417269c8cc2fdb4 Mon Sep 17 00:00:00 2001 From: adro Date: Thu, 17 Feb 2022 13:31:01 +0100 Subject: [PATCH] MVP #1 --- .gitignore | 1 + config/config.go | 63 ++++++++++++++++++++++++++++ go.mod | 6 +++ go.sum | 8 ++++ lib/packet.go | 50 ++++++++++++++++++++++ main.go | 65 +++++++++++------------------ web/auth.go | 78 +++++++++++++++++++++++++++++++++++ web/device.go | 34 +++++++++++++++ web/index.go | 39 ++++++++++++++++++ web/public/style.css | 0 web/template/device.html.tmpl | 21 ++++++++++ web/template/login.html.tmpl | 3 ++ web/template/page.html.tmpl | 13 ++++++ web/wake.go | 29 +++++++++++++ web/web.go | 55 ++++++++++++++++++++++++ 15 files changed, 425 insertions(+), 40 deletions(-) create mode 100644 config/config.go create mode 100644 go.sum create mode 100644 lib/packet.go create mode 100644 web/auth.go create mode 100644 web/device.go create mode 100644 web/index.go create mode 100644 web/public/style.css create mode 100644 web/template/device.html.tmpl create mode 100644 web/template/login.html.tmpl create mode 100644 web/template/page.html.tmpl create mode 100644 web/wake.go create mode 100644 web/web.go diff --git a/.gitignore b/.gitignore index adf8f72..d5236fb 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ # Go workspace file go.work +config.toml diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..acc6579 --- /dev/null +++ b/config/config.go @@ -0,0 +1,63 @@ +package config + +import ( + "bytes" + "os" + + "github.com/BurntSushi/toml" +) + +type Device struct { + Alias string + MAC string + IP string +} + +type config struct { + Server string + PassHash string + Device []Device +} + +var Config config + +var configPath string + +func init() { + Config = config{ + Server: ":8080", + } + + // Locations to look for a config file for + checkPaths := []string{ + "config.toml", + "config/config.toml", + "/etc/miniwol/config.toml", + } + + var err error + + for _, path := range checkPaths { + _, err = toml.DecodeFile(path, &Config) + if err == nil { + configPath = path + return + } + } + panic(err) +} + +func Save() error { + var buffer bytes.Buffer + var err error + encoder := toml.NewEncoder(&buffer) + err = encoder.Encode(Config) + if err != nil { + return err + } + err = os.WriteFile(configPath, buffer.Bytes(), 0666) + if err != nil { + return err + } + return nil +} diff --git a/go.mod b/go.mod index 86401cb..a5cd95d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module miniwol go 1.17 + +require ( + github.com/BurntSushi/toml v1.0.0 + github.com/google/uuid v1.3.0 + golang.org/x/crypto v0.0.0-20220214200702-86341886e292 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4c8aaa0 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= +github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= diff --git a/lib/packet.go b/lib/packet.go new file mode 100644 index 0000000..3769b0d --- /dev/null +++ b/lib/packet.go @@ -0,0 +1,50 @@ +package lib + +import ( + "net" +) + +// Sends a magic packet +func SendPacket(from, to, macStr string) error { + // Get a binary representation of that MAC Address + mac, err := net.ParseMAC(macStr) + if err != nil { + return err + } + + // Construct the Magic Packet + packet := make([]byte, 102) + index := 0 + for i := 0; i < 6; i++ { + packet[index] = 0xFF + index++ + } + for i := 0; i < 16; i++ { + for j := 0; j < 6; j++ { + packet[index] = mac[j] + index++ + } + } + + // Resolve addresses + localAddr, err := net.ResolveUDPAddr("udp4", from) + if err != nil { + return err + } + + broadcastAddr, err := net.ResolveUDPAddr("udp4", to) + if err != nil { + return err + } + + // Send packet + conn, err := net.DialUDP("udp4", localAddr, broadcastAddr) + if err != nil { + return err + } + defer conn.Close() + + conn.Write(packet) + + return nil +} diff --git a/main.go b/main.go index d300a57..57a6810 100644 --- a/main.go +++ b/main.go @@ -2,50 +2,35 @@ package main import ( "fmt" - "net" + "log" + "miniwol/config" + "miniwol/web" + "os" + + "golang.org/x/crypto/bcrypt" ) func main() { - macStr := "" // Target Computers MAC Address - - // Get a binary representation of that MAC Address - mac, err := net.ParseMAC(macStr) - if err != nil { - panic(err) + if len(os.Args) < 2 { + os.Args = append(os.Args, "") } - - // Construct the Magic Packet - packet := make([]byte, 102) - index := 0 - for i := 0; i < 6; i++ { - packet[index] = 0xFF - index++ - } - for i := 0; i < 16; i++ { - for j := 0; j < 6; j++ { - packet[index] = mac[j] - index++ + switch os.Args[1] { + case "setpass": + passHash, err := bcrypt.GenerateFromPassword([]byte(os.Args[2]), bcrypt.DefaultCost) + if err != nil { + log.Fatal(err) } + config.Config.PassHash = string(passHash) + err = config.Save() + if err != nil { + log.Fatal(err) + } + fmt.Println("Successfully set password") + case "web": + fallthrough + case "": + log.Fatal(web.Run()) + default: + fmt.Println("Unknown subcommand", os.Args[1]) } - - localAddr, err := net.ResolveUDPAddr("udp4", ":9") - if err != nil { - panic(err) - } - - broadcastAddr, err := net.ResolveUDPAddr("udp4", "255.255.255.255:9") // General Broadcast - // broadcastAddr, err := net.ResolveUDPAddr("udp4", "192.168.178.255:9") // Broadcast on most home networks - if err != nil { - panic(err) - } - - conn, err := net.DialUDP("udp4", localAddr, broadcastAddr) - if err != nil { - panic(err) - } - - defer conn.Close() - conn.Write(packet) - - fmt.Println("Successfully sent magic packet") } diff --git a/web/auth.go b/web/auth.go new file mode 100644 index 0000000..1042a5f --- /dev/null +++ b/web/auth.go @@ -0,0 +1,78 @@ +package web + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +var sessions map[string]time.Time + +func init() { + sessions = make(map[string]time.Time) +} + +func auth(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "POST": + password := r.FormValue("password") + if bcrypt.CompareHashAndPassword([]byte("$2a$04$i4bdOiia2YFN7JXfXLgO4ONCffC67ECyzPEcTLzoP3Lzse/sZT5EC"), []byte(password)) != nil { + w.WriteHeader(401) + w.Write([]byte("Wrong Password")) + return + } + token := uuid.New().String() + sessions[token] = time.Now().Add(time.Hour * 24) + w.Header().Add("Set-Cookie", fmt.Sprintf("session=%s; Path=/; SameSite=Strict; HttpOnly; Secure", token)) + http.Redirect(w, r, "/device", http.StatusTemporaryRedirect) + default: + w.WriteHeader(http.StatusMethodNotAllowed) // Method not Allowed + } +} + +func deauth(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "POST": + cookie, err := r.Cookie("session") + if err != nil { + w.WriteHeader(400) + return + } + + token := cookie.Value + if isAuthenticated(token) == nil { + delete(sessions, token) + } + default: + w.WriteHeader(405) + } +} + +func checkAuthentication(w http.ResponseWriter, r *http.Request) error { + sCookie, err := r.Cookie("") + if err == nil && isAuthenticated(sCookie.Value) == nil { + return nil + } + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return errors.New("authentication error") +} + +func isAuthenticated(token string) error { + for sToken, expiree := range sessions { + // Expire old sessions + if time.Now().After(expiree) { + delete(sessions, sToken) + continue + } + // Check for valid session of token + if token == sToken { + return nil + } + } + + return errors.New("this token is not associated with a valid session") +} diff --git a/web/device.go b/web/device.go new file mode 100644 index 0000000..7060bd2 --- /dev/null +++ b/web/device.go @@ -0,0 +1,34 @@ +package web + +import ( + "bytes" + "html/template" + "miniwol/config" + "net/http" +) + +func device(w http.ResponseWriter, r *http.Request) { + if checkAuthentication(w, r) != nil { + return + } + + page := struct { + Title string + Content template.HTML + }{ + Title: "Devices", + } + + var contentBuffer bytes.Buffer + err := deviceTemplate.Execute(&contentBuffer, config.Config.Device) + if err != nil { + panic(err) + } + + page.Content = template.HTML(contentBuffer.String()) + + err = pageTemplate.Execute(w, page) + if err != nil { + panic(err) + } +} diff --git a/web/index.go b/web/index.go new file mode 100644 index 0000000..4b4bd00 --- /dev/null +++ b/web/index.go @@ -0,0 +1,39 @@ +package web + +import ( + "bytes" + "html/template" + "net/http" +) + +func index(w http.ResponseWriter, r *http.Request) { + // Serve static files + if r.URL.Path != "/" { + fileServer.ServeHTTP(w, r) + return + } + + page := struct { + Title string + Content template.HTML + }{} + var contentBuffer bytes.Buffer + + sCookie, err := r.Cookie("session") + if err == nil && isAuthenticated(sCookie.Value) == nil { + page.Title = "Miniwol" + contentBuffer.WriteString("TODO") + } else { + page.Title = "Login" + err = loginTemplate.Execute(&contentBuffer, struct{}{}) + if err != nil { + panic(err) + } + } + + page.Content = template.HTML(contentBuffer.String()) + err = pageTemplate.Execute(w, page) + if err != nil { + panic(err) + } +} diff --git a/web/public/style.css b/web/public/style.css new file mode 100644 index 0000000..e69de29 diff --git a/web/template/device.html.tmpl b/web/template/device.html.tmpl new file mode 100644 index 0000000..5d190e7 --- /dev/null +++ b/web/template/device.html.tmpl @@ -0,0 +1,21 @@ + + + + + + + + {{range .}} + + + + + + + {{end}} +
Device AliasMAC AddressIP/BroadcastActions
{{.Alias}}{{.MAC}}{{.IP}} +
+ + +
+
\ No newline at end of file diff --git a/web/template/login.html.tmpl b/web/template/login.html.tmpl new file mode 100644 index 0000000..f12ac23 --- /dev/null +++ b/web/template/login.html.tmpl @@ -0,0 +1,3 @@ +
+ +
diff --git a/web/template/page.html.tmpl b/web/template/page.html.tmpl new file mode 100644 index 0000000..d7ca027 --- /dev/null +++ b/web/template/page.html.tmpl @@ -0,0 +1,13 @@ + + + + + + + + {{ .Title }} + + + {{ .Content }} + + diff --git a/web/wake.go b/web/wake.go new file mode 100644 index 0000000..a1d2692 --- /dev/null +++ b/web/wake.go @@ -0,0 +1,29 @@ +package web + +import ( + "miniwol/config" + "miniwol/lib" + "net/http" + "strings" +) + +func wake(w http.ResponseWriter, r *http.Request) { + if checkAuthentication(w, r) != nil { + return + } + + for _, device := range config.Config.Device { + if r.FormValue("alias") == device.Alias { + if !strings.Contains(device.IP, ":") { + device.IP += ":9" + } + err := lib.SendPacket(":0", device.IP, device.MAC) + if err != nil { + panic(err) + } + + w.Write([]byte("Successfuly woke up " + device.Alias)) + return + } + } +} diff --git a/web/web.go b/web/web.go new file mode 100644 index 0000000..cb45b9c --- /dev/null +++ b/web/web.go @@ -0,0 +1,55 @@ +package web + +import ( + "embed" + "html/template" + "io/fs" + "log" + "miniwol/config" + "net/http" +) + +//go:embed template/* +var templateFS embed.FS +var pageTemplate *template.Template +var loginTemplate *template.Template +var deviceTemplate *template.Template + +//go:embed public +var publicFS embed.FS +var fileServer http.Handler + +func init() { + var err error + + pageTemplate, err = template.ParseFS(templateFS, "template/page.html.tmpl") + if err != nil { + panic(err) + } + loginTemplate, err = template.ParseFS(templateFS, "template/login.html.tmpl") + if err != nil { + panic(err) + } + deviceTemplate, err = template.ParseFS(templateFS, "template/device.html.tmpl") + if err != nil { + panic(err) + } + + // Static file handler + staticContent, err := fs.Sub(publicFS, "public") + if err != nil { + panic(err) + } + fileServer = http.FileServer(http.FS(staticContent)) +} + +func Run() error { + http.HandleFunc("/auth", auth) + http.HandleFunc("/deauth", deauth) + http.HandleFunc("/device", device) + http.HandleFunc("/wake", wake) + http.HandleFunc("/", index) + + log.Println("Starting Webserver on", config.Config.Server) + return http.ListenAndServe(config.Config.Server, nil) +}