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.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
|
||||
|
||||
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
|
||||
}
|
65
main.go
65
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")
|
||||
}
|
||||
|
|
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