adro
2 years ago
15 changed files with 425 additions and 40 deletions
@ -0,0 +1,63 @@
@@ -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 |
||||
} |
@ -1,3 +1,9 @@
@@ -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 |
||||
) |
||||
|
@ -0,0 +1,8 @@
@@ -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= |
@ -0,0 +1,50 @@
@@ -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 |
||||
} |
@ -0,0 +1,78 @@
@@ -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") |
||||
} |
@ -0,0 +1,34 @@
@@ -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) |
||||
} |
||||
} |
@ -0,0 +1,39 @@
@@ -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) |
||||
} |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
<table> |
||||
<tr> |
||||
<th>Device Alias</th> |
||||
<th>MAC Address</th> |
||||
<th>IP/Broadcast</th> |
||||
<th>Actions</th> |
||||
</tr> |
||||
{{range .}} |
||||
<tr> |
||||
<td>{{.Alias}}</td> |
||||
<td>{{.MAC}}</td> |
||||
<td>{{.IP}}</td> |
||||
<td> |
||||
<form action="/wake" method="post"> |
||||
<input type="text" name="alias" value="{{.Alias}}" hidden> |
||||
<input type="submit" value="Wake"> |
||||
</form> |
||||
</td> |
||||
</tr> |
||||
{{end}} |
||||
</table> |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
<form action="/auth" method="post"> |
||||
<label for="pw">Password: </label><input type="password" name="password"> |
||||
</form> |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html> |
||||
<html> |
||||
<head> |
||||
<meta charset="UTF-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"> |
||||
<link rel="stylesheet" href="style.css"> |
||||
<title>{{ .Title }}</title> |
||||
</head> |
||||
<body> |
||||
{{ .Content }} |
||||
</body> |
||||
</html> |
@ -0,0 +1,29 @@
@@ -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 |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,55 @@
@@ -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) |
||||
} |
Loading…
Reference in new issue