diff --git a/config/config.go b/config/config.go index 830fb8f..e179641 100644 --- a/config/config.go +++ b/config/config.go @@ -8,16 +8,16 @@ import ( ) type Device struct { - Alias string - MAC string - IP string + Alias string `form:"Alias"` + MAC string `form:"MAC"` + IP string `form:"IP"` } type config struct { Server string PassHash string SessionTTL float64 - Device []Device + Devices []Device } var Config config diff --git a/example/config.toml b/example/config.toml index 7ac5149..779ab5b 100644 --- a/example/config.toml +++ b/example/config.toml @@ -2,12 +2,12 @@ Server = ":8080" # The address the webserver should bind to PassHash = "$2a$10$I.26oCzkjZ8qwfhbmeYM3.kppBjxtPsxkeE1Y.ULjVvA1IBPcQP42" # "password" SessionTTL = 60 # How many minutes sessions last for -[[Device]] +[[Devices]] Alias = "SomeDevice" MAC = "DE-AD-BE-EF-F0-05" # Delimiter dashes/colons, upper/lowercase IP = "192.168.178.255" # Broadcast for most home networks -[[Device]] +[[Devices]] Alias = "Another Device" MAC = "" IP = "" \ No newline at end of file diff --git a/web/auth.go b/web/auth.go index 867128e..91500ac 100644 --- a/web/auth.go +++ b/web/auth.go @@ -5,7 +5,11 @@ import ( "net/http" "time" + "git.ulra.eu/adro/miniwol/config" + + "github.com/google/uuid" "github.com/labstack/echo/v4" + "golang.org/x/crypto/bcrypt" ) var sessions map[string]time.Time @@ -39,3 +43,33 @@ func withAuth(handler echo.HandlerFunc) echo.HandlerFunc { return handler(c) } } + +// Handlers +func auth(c echo.Context) error { + password := c.FormValue("Password") + if bcrypt.CompareHashAndPassword([]byte(config.Config.PassHash), []byte(password)) != nil { + return c.String(401, "Wrong Password") + } + token := uuid.New().String() + sessions[token] = time.Now().Add(time.Second * time.Duration(config.Config.SessionTTL*60)) + c.SetCookie(&http.Cookie{ + Name: "session", + Value: token, + Path: "/", + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + Expires: sessions[token], + }) + return c.Redirect(http.StatusSeeOther, "/") +} + +func deauth(c echo.Context) error { + session, err := c.Cookie("session") + if err != nil { + return err + } + + delete(sessions, session.Value) + return c.Redirect(http.StatusSeeOther, "/") +} diff --git a/web/device.go b/web/device.go new file mode 100644 index 0000000..f78dcf4 --- /dev/null +++ b/web/device.go @@ -0,0 +1,68 @@ +package web + +import ( + "errors" + "net/http" + "strings" + + "git.ulra.eu/adro/miniwol/config" + "git.ulra.eu/adro/miniwol/lib" + + "github.com/labstack/echo/v4" +) + +func add(c echo.Context) error { + device := config.Device{} + err := c.Bind(&device) + if err != nil { + return err + } + + config.Config.Devices = append(config.Config.Devices, device) + config.Save() + if err != nil { + return err + } + return c.Redirect(http.StatusSeeOther, "/") +} + +func wake(c echo.Context) error { + _device := config.Device{} + err := c.Bind(&_device) + if err != nil { + return err + } + for _, device := range config.Config.Devices { + if device == _device { + if !strings.Contains(device.IP, ":") { + device.IP += ":9" + } + err := lib.SendPacket(":0", device.IP, device.MAC) + if err != nil { + return err + } + + return c.Redirect(http.StatusSeeOther, "/") + } + } + return errors.New("device not found") +} + +func remove(c echo.Context) error { + _device := config.Device{} + err := c.Bind(&_device) + if err != nil { + return err + } + for i, device := range config.Config.Devices { + if device == _device { + config.Config.Devices = append(config.Config.Devices[:i], config.Config.Devices[i+1:]...) + err := config.Save() + if err != nil { + return err + } + return c.Redirect(http.StatusSeeOther, "/") + } + } + return errors.New("device not found") +} diff --git a/web/template/device.html b/web/template/device.html new file mode 100644 index 0000000..328c463 --- /dev/null +++ b/web/template/device.html @@ -0,0 +1,51 @@ +
+
+ Add Device + + + + + +
+
+ +
+ Devices + + + + + + + + + {{range $i, $d := .Devices}} + + + + + + + {{end}} +
AliasMACIP/BroadcastActions
{{$d.Alias}}{{$d.MAC}}{{$d.IP}} +
+ + + + +
+
+ + + + +
+
+
+ + \ No newline at end of file diff --git a/web/template/device.html.tmpl b/web/template/device.html.tmpl deleted file mode 100644 index 1d6a655..0000000 --- a/web/template/device.html.tmpl +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - {{range $i, $d := .Device}} - - - - - - - {{end}} -
Device AliasMAC AddressIP/BroadcastActions
{{$d.Alias}}{{$d.MAC}}{{$d.IP}} -
- - - -
-
\ No newline at end of file diff --git a/web/template/login.html b/web/template/login.html new file mode 100644 index 0000000..9438442 --- /dev/null +++ b/web/template/login.html @@ -0,0 +1,8 @@ +
+
+ Login + + + +
+
diff --git a/web/template/login.html.tmpl b/web/template/login.html.tmpl deleted file mode 100644 index f12ac23..0000000 --- a/web/template/login.html.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -
- -
diff --git a/web/template/page.html b/web/template/page.html new file mode 100644 index 0000000..ed1f961 --- /dev/null +++ b/web/template/page.html @@ -0,0 +1,24 @@ + + + + + + + + + + {{ .Title }} - miniwol + + + +
{{ .Content }}
+ + diff --git a/web/template/page.html.tmpl b/web/template/page.html.tmpl deleted file mode 100644 index 4976644..0000000 --- a/web/template/page.html.tmpl +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - {{ .Title }} - - - {{ .Content }} - - diff --git a/web/template/style.css b/web/template/style.css new file mode 100644 index 0000000..3d28fe3 --- /dev/null +++ b/web/template/style.css @@ -0,0 +1,96 @@ +* { + padding: 0; + margin: 0; + box-sizing: border-box; + color: inherit; + line-height: 1; +} + +nav, div, fieldset { + display: flex; + flex-flow: column nowrap; + padding: 0.5rem; + gap: 0.5rem; +} + +nav { + flex-flow: row nowrap; +} + +fieldset { + display: grid; + grid-template-columns: auto 1fr; + flex-grow: 1; + + overflow-x: auto; +} + +fieldset > label:after { + content: ":"; + pointer-events: none; +} + +form { + max-width: max-content; +} + +input[type=submit] { + grid-column: 1 / 3; + padding: 0.25rem; + cursor: pointer; +} + +.actions { + text-align: center; +} + +.actions > form { + display: contents; +} + +table { + grid-column: 1 / 3; + border-collapse: collapse; +} + +legend { + font-weight: bold; +} + +/* Colors */ + +:root { + --cl-fg: #222; + --cl-bg-page: #eee; + --cl-bg-block: #ddd; + --cl-bg-input: #ccc; +} + +@media (prefers-color-scheme: dark) { + :root { + --cl-fg: #ddd; + --cl-bg-page: #333; + --cl-bg-block: #222; + --cl-bg-input: #444; + } +} + +:root { + font-family: sans-serif; + background: var(--cl-bg-page); + color: var(--cl-fg); +} + +nav, fieldset, table { + background: var(--cl-bg-block); +} + +input { + border: 1px solid var(--cl-fg); + background: var(--cl-bg-input); +} + +input[type=submit]:hover, +tr:nth-child(even) { + background: var(--cl-bg-page); +} diff --git a/web/web.go b/web/web.go index 98dd087..39b13d5 100644 --- a/web/web.go +++ b/web/web.go @@ -3,19 +3,11 @@ package web import ( "bytes" "embed" - "errors" - "fmt" "html/template" - "net/http" - "strings" - "time" "git.ulra.eu/adro/miniwol/config" - "git.ulra.eu/adro/miniwol/lib" - "github.com/google/uuid" "github.com/labstack/echo/v4" - "golang.org/x/crypto/bcrypt" ) //go:embed template/* @@ -25,7 +17,7 @@ var templates *template.Template func init() { var err error - templates, err = template.ParseFS(templateFS, "template/*.html.tmpl") + templates, err = template.ParseFS(templateFS, "template/*.html") if err != nil { panic(err) } @@ -35,25 +27,31 @@ func Run() error { e := echo.New() e.GET("/", index) + e.GET("/style.css", style) e.POST("/auth", auth) + e.POST("/deauth", withAuth(deauth)) + e.POST("/add", withAuth(add)) e.POST("/wake", withAuth(wake)) + e.POST("/remove", withAuth(remove)) return e.Start(config.Config.Server) } -func Page(c echo.Context, code int, title string, page string, data interface{}) error { +func Page(c echo.Context, code int, title string, page string, auth bool, data interface{}) error { var contentBuffer bytes.Buffer err := templates.ExecuteTemplate(&contentBuffer, page, data) if err != nil { return err } var pageBuffer bytes.Buffer - err = templates.ExecuteTemplate(&pageBuffer, "page.html.tmpl", struct { + err = templates.ExecuteTemplate(&pageBuffer, "page.html", struct { Title string Content template.HTML + Auth bool }{ Title: title, Content: template.HTML(contentBuffer.String()), + Auth: auth, }) if err != nil { return err @@ -65,44 +63,16 @@ func Page(c echo.Context, code int, title string, page string, data interface{}) func index(c echo.Context) error { session, err := c.Cookie("session") if err != nil || checkAuth(session.Value) != nil { - return Page(c, 200, "Login", "login.html.tmpl", nil) + return Page(c, 200, "Login", "login.html", false, nil) } else { - return Page(c, 200, "Device", "device.html.tmpl", config.Config) + return Page(c, 200, "Devices", "device.html", true, config.Config) } } -func auth(c echo.Context) error { - password := c.FormValue("password") - if bcrypt.CompareHashAndPassword([]byte(config.Config.PassHash), []byte(password)) != nil { - return c.String(401, "Wrong Password") +func style(c echo.Context) error { + styleData, err := templateFS.ReadFile("template/style.css") + if err != nil { + return err } - token := uuid.New().String() - sessions[token] = time.Now().Add(time.Second * time.Duration(config.Config.SessionTTL*60)) - c.SetCookie(&http.Cookie{ - Name: "session", - Value: token, - Path: "/", - Secure: true, - HttpOnly: true, - SameSite: http.SameSiteStrictMode, - Expires: sessions[token], - }) - return c.Redirect(http.StatusSeeOther, "/") -} - -func wake(c echo.Context) error { - for i, device := range config.Config.Device { - if c.FormValue("alias") == device.Alias && c.FormValue("index") == fmt.Sprint(i) { - if !strings.Contains(device.IP, ":") { - device.IP += ":9" - } - err := lib.SendPacket(":0", device.IP, device.MAC) - if err != nil { - return err - } - - return c.Redirect(http.StatusSeeOther, "/") - } - } - return errors.New("device not found") + return c.Blob(200, "text/css", styleData) }