This commit is contained in:
adro 2022-02-17 13:31:01 +01:00
parent 34b2fa0e74
commit 84d396cfb6
15 changed files with 425 additions and 40 deletions

1
.gitignore vendored
View File

@ -21,3 +21,4 @@
# Go workspace file # Go workspace file
go.work go.work
config.toml

63
config/config.go Normal file
View File

@ -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
}

6
go.mod
View File

@ -1,3 +1,9 @@
module miniwol module miniwol
go 1.17 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
)

8
go.sum Normal file
View File

@ -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=

50
lib/packet.go Normal file
View File

@ -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
}

65
main.go
View File

@ -2,50 +2,35 @@ package main
import ( import (
"fmt" "fmt"
"net" "log"
"miniwol/config"
"miniwol/web"
"os"
"golang.org/x/crypto/bcrypt"
) )
func main() { func main() {
macStr := "" // Target Computers MAC Address if len(os.Args) < 2 {
os.Args = append(os.Args, "")
// Get a binary representation of that MAC Address
mac, err := net.ParseMAC(macStr)
if err != nil {
panic(err)
} }
switch os.Args[1] {
// Construct the Magic Packet case "setpass":
packet := make([]byte, 102) passHash, err := bcrypt.GenerateFromPassword([]byte(os.Args[2]), bcrypt.DefaultCost)
index := 0 if err != nil {
for i := 0; i < 6; i++ { log.Fatal(err)
packet[index] = 0xFF
index++
}
for i := 0; i < 16; i++ {
for j := 0; j < 6; j++ {
packet[index] = mac[j]
index++
} }
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")
} }

78
web/auth.go Normal file
View File

@ -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")
}

34
web/device.go Normal file
View File

@ -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)
}
}

39
web/index.go Normal file
View File

@ -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
web/public/style.css Normal file
View File

View File

@ -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>

View File

@ -0,0 +1,3 @@
<form action="/auth" method="post">
<label for="pw">Password: </label><input type="password" name="password">
</form>

View File

@ -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>

29
web/wake.go Normal file
View File

@ -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
}
}
}

55
web/web.go Normal file
View File

@ -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)
}