MVP #1
This commit is contained in:
parent
34b2fa0e74
commit
84d396cfb6
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -21,3 +21,4 @@
|
||||||
# Go workspace file
|
# Go workspace file
|
||||||
go.work
|
go.work
|
||||||
|
|
||||||
|
config.toml
|
||||||
|
|
63
config/config.go
Normal file
63
config/config.go
Normal 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
6
go.mod
|
@ -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
8
go.sum
Normal 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
50
lib/packet.go
Normal 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
|
||||||
|
}
|
61
main.go
61
main.go
|
@ -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)
|
switch os.Args[1] {
|
||||||
|
case "setpass":
|
||||||
|
passHash, err := bcrypt.GenerateFromPassword([]byte(os.Args[2]), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
config.Config.PassHash = string(passHash)
|
||||||
// Construct the Magic Packet
|
err = config.Save()
|
||||||
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++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
localAddr, err := net.ResolveUDPAddr("udp4", ":9")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
fmt.Println("Successfully set password")
|
||||||
broadcastAddr, err := net.ResolveUDPAddr("udp4", "255.255.255.255:9") // General Broadcast
|
case "web":
|
||||||
// broadcastAddr, err := net.ResolveUDPAddr("udp4", "192.168.178.255:9") // Broadcast on most home networks
|
fallthrough
|
||||||
if err != nil {
|
case "":
|
||||||
panic(err)
|
log.Fatal(web.Run())
|
||||||
|
default:
|
||||||
|
fmt.Println("Unknown subcommand", os.Args[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
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
78
web/auth.go
Normal 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
34
web/device.go
Normal 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
39
web/index.go
Normal 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
0
web/public/style.css
Normal file
21
web/template/device.html.tmpl
Normal file
21
web/template/device.html.tmpl
Normal 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>
|
3
web/template/login.html.tmpl
Normal file
3
web/template/login.html.tmpl
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<form action="/auth" method="post">
|
||||||
|
<label for="pw">Password: </label><input type="password" name="password">
|
||||||
|
</form>
|
13
web/template/page.html.tmpl
Normal file
13
web/template/page.html.tmpl
Normal 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
29
web/wake.go
Normal 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
55
web/web.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user