Compare commits

..

No commits in common. "backup" and "main" have entirely different histories.
backup ... main

15 changed files with 142 additions and 547 deletions

View file

@ -4,7 +4,8 @@ import "github.com/google/uuid"
type EFIApp struct { type EFIApp struct {
Name string `json:"name"` Name string `json:"name"`
DevicePath string `json:"device_path"` Path string `json:"path"`
DiskID string `json:"disk_id"`
} }
type Client struct { type Client struct {

View file

@ -6,7 +6,6 @@ import (
"encoding" "encoding"
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -17,12 +16,11 @@ const (
ActionRequest Action = iota ActionRequest Action = iota
ActionAccept ActionAccept
ActionDeny ActionDeny
ActionDiscover
ActionUnknown ActionUnknown
) )
var spaceByte = []byte(" ") var spaceByte = []byte(" ")
var commaByte = []byte(";") var commaByte = []byte(",")
const ( const (
keyID = "id" keyID = "id"
@ -43,8 +41,6 @@ func (a Action) String() string {
return "BOOT_DENY" return "BOOT_DENY"
case ActionRequest: case ActionRequest:
return "BOOT_REQUEST" return "BOOT_REQUEST"
case ActionDiscover:
return "BOOT_DISCOVER"
default: default:
return "unknown" return "unknown"
} }
@ -58,8 +54,6 @@ func newActionFromBytes(raw []byte) Action {
return ActionDeny return ActionDeny
case "BOOT_REQUEST": case "BOOT_REQUEST":
return ActionRequest return ActionRequest
case "BOOT_DISCOVER":
return ActionDiscover
default: default:
return ActionUnknown return ActionUnknown
} }
@ -146,13 +140,9 @@ func (am *acceptMessage) UnmarshalBinary(data []byte) error {
func (am *acceptMessage) MarshalBinary() (data []byte, err error) { func (am *acceptMessage) MarshalBinary() (data []byte, err error) {
action := []byte(am.Action().String()) action := []byte(am.Action().String())
// efiApp := strings.ReplaceAll(am.efiApp, `\`, `\\`)
// efiApp = strings.ReplaceAll(efiApp, "File(", "")
efiApp := strings.ReplaceAll(am.efiApp, "File(", "")
efiApp, _ = strings.CutSuffix(efiApp, ")")
params := [][]byte{ params := [][]byte{
[]byte(fmt.Sprintf("%s=%s", keyID, am.id.String())), []byte(fmt.Sprintf("%s=%s", keyID, am.id.String())),
[]byte(fmt.Sprintf("%s=%s", keyEfiApp, efiApp)), []byte(fmt.Sprintf("%s=%s", keyEfiApp, am.efiApp)),
} }
param_bytes := bytes.Join(params, commaByte) param_bytes := bytes.Join(params, commaByte)
return bytes.Join([][]byte{action, param_bytes}, spaceByte), nil return bytes.Join([][]byte{action, param_bytes}, spaceByte), nil
@ -222,30 +212,11 @@ func (dm *denyMessage) String() string {
return fmt.Sprintf("%s from %s, reason %q", ActionDeny.String(), dm.ID().String(), dm.reason) return fmt.Sprintf("%s from %s, reason %q", ActionDeny.String(), dm.ID().String(), dm.reason)
} }
type discoverMessage struct{}
func (dm *discoverMessage) UnmarshalBinary(data []byte) error {
return nil
}
func (dm *discoverMessage) MarshalBinary() (data []byte, err error) {
return []byte(dm.Action().String()), nil
}
func (dm *discoverMessage) Action() Action {
return ActionDiscover
}
func (dm *discoverMessage) ID() uuid.UUID {
return uuid.Nil
}
func (dm *discoverMessage) String() string {
return ActionDiscover.String()
}
func MessageFromBytes(dat []byte) (Message, error) { func MessageFromBytes(dat []byte) (Message, error) {
rawAction, content, _ := bytes.Cut(dat, spaceByte) rawAction, content, found := bytes.Cut(dat, spaceByte)
if !found {
return nil, ErrInvalidFormat
}
var message Message var message Message
action := newActionFromBytes(rawAction) action := newActionFromBytes(rawAction)
@ -256,14 +227,12 @@ func MessageFromBytes(dat []byte) (Message, error) {
message = &acceptMessage{} message = &acceptMessage{}
case ActionDeny: case ActionDeny:
message = &denyMessage{} message = &denyMessage{}
case ActionDiscover:
message = &discoverMessage{}
default: default:
return nil, ErrUnknownAction return nil, ErrUnknownAction
} }
if err := message.UnmarshalBinary(content); err != nil { if err := message.UnmarshalBinary(content); err != nil {
return nil, fmt.Errorf("failed to parse %s message: %w", message.Action().String(), err) return nil, fmt.Errorf("failed to parse message: %w", err)
} }
return message, nil return message, nil
} }

View file

@ -44,45 +44,30 @@ type jsonConf struct {
} `json:"log"` } `json:"log"`
Storage struct { Storage struct {
Path string `json:"path"` Path string `json:"path"`
TemplateDir string `json:"templatePath"`
StaticDir string `json:"staticPath"`
} `json:"storage"` } `json:"storage"`
Server struct { Server struct {
Host string `json:"host"` Host string `json:"host"`
Port int `json:"port"` Port int `json:"port"`
Mode string `json:"mode"` Mode string `json:"mode"`
SockPath string `json:"sock"` SockPath string `json:"sock"`
PublicHost string `json:"public_host"`
} `json:"server"` } `json:"server"`
BootProvider struct { BootProvider struct {
Iface string `json:"interface"` Iface string `json:"interface"`
Port int `json:"port"` Port int `json:"port"`
McastGroup string `json:"multicast_group"` McastGroup string `json:"multicast_group"`
SrcAddr string `json:"src_addr"`
} `json:"boot_provider"` } `json:"boot_provider"`
HomeAssistant struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
APIToken string `json:"token"`
} `json:"home_assistant"`
} }
type AppConfig struct { type AppConfig struct {
LogLevel logrus.Level LogLevel logrus.Level
ServerMode ListeningMode ServerMode ListeningMode
DataFilepath string DataFilepath string
StaticDir string
Host string Host string
PublicHost string
Port int Port int
SockPath string SockPath string
UPDMcastGroup string UPDMcastGroup string
UDPPort int UDPPort int
UDPIface string UDPIface string
UDPSrcAddr string
HomeAssistantEnabled bool
HomeAssistantHost string
HomeAssistantToken string
} }
func parseLevel(lvlStr string) logrus.Level { func parseLevel(lvlStr string) logrus.Level {
@ -108,17 +93,11 @@ func (ac *AppConfig) UnmarshalJSON(data []byte) error {
ac.ServerMode = lm ac.ServerMode = lm
ac.SockPath = jsonConf.Server.SockPath ac.SockPath = jsonConf.Server.SockPath
ac.Host = jsonConf.Server.Host ac.Host = jsonConf.Server.Host
ac.PublicHost = jsonConf.Server.PublicHost
ac.Port = jsonConf.Server.Port ac.Port = jsonConf.Server.Port
ac.UPDMcastGroup = jsonConf.BootProvider.McastGroup ac.UPDMcastGroup = jsonConf.BootProvider.McastGroup
ac.UDPIface = jsonConf.BootProvider.Iface ac.UDPIface = jsonConf.BootProvider.Iface
ac.UDPPort = jsonConf.BootProvider.Port ac.UDPPort = jsonConf.BootProvider.Port
ac.UDPSrcAddr = jsonConf.BootProvider.SrcAddr
ac.DataFilepath = jsonConf.Storage.Path ac.DataFilepath = jsonConf.Storage.Path
ac.StaticDir = jsonConf.Storage.StaticDir
ac.HomeAssistantEnabled = jsonConf.HomeAssistant.Enabled
ac.HomeAssistantHost = jsonConf.HomeAssistant.Host
ac.HomeAssistantToken = jsonConf.HomeAssistant.APIToken
return nil return nil
} }
@ -127,12 +106,10 @@ var defaultConfig AppConfig = AppConfig{
ServerMode: ModeNet, ServerMode: ModeNet,
DataFilepath: "boot_options.json", DataFilepath: "boot_options.json",
Host: "0.0.0.0", Host: "0.0.0.0",
PublicHost: "http://127.0.0.1:5000",
Port: 5000, Port: 5000,
UPDMcastGroup: "ff02::abcd:1234", UPDMcastGroup: "ff02::abcd:1234",
UDPPort: 42, UDPPort: 42,
UDPIface: "eth0", UDPIface: "eth0",
StaticDir: "./static",
} }
func New(filepath string) (*AppConfig, error) { func New(filepath string) (*AppConfig, error) {

View file

@ -1,126 +1,85 @@
package client package client
import ( // import (
"encoding/json" // "encoding/json"
"errors" // "fmt"
"fmt" // "io"
"io" // "net"
"net/http" // "net/http"
"git.faercol.me/faercol/http-boot-server/bootserver/config" // "git.faercol.me/faercol/http-boot-server/bootserver/helpers"
"git.faercol.me/faercol/http-boot-server/bootserver/helpers" // "git.faercol.me/faercol/http-boot-server/bootserver/services"
"git.faercol.me/faercol/http-boot-server/bootserver/homeassistant" // "github.com/sirupsen/logrus"
"git.faercol.me/faercol/http-boot-server/bootserver/services" // )
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
const SetBootRoute = "/config/boot" // const BootRoute = "/boot"
type setBootOptionPayload struct { // type BootController struct {
ClientID string `json:"client_id"` // clientService *services.ClientHandlerService
OptionID string `json:"option_id"` // l *logrus.Logger
} // }
type BootController struct { // func NewBootController(logger *logrus.Logger, service *services.ClientHandlerService) *BootController {
clientService *services.ClientHandlerService // return &BootController{
appConf *config.AppConfig // clientService: service,
l *logrus.Logger // l: logger,
} // }
// }
func NewBootController(logger *logrus.Logger, service *services.ClientHandlerService, conf *config.AppConfig) *BootController { // func (bc *BootController) getBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, []byte, error) {
return &BootController{ // bootOption, err := bc.clientService.GetClientSelectedBootOption(clientIP)
clientService: service, // if err != nil {
l: logger, // return http.StatusInternalServerError, nil, fmt.Errorf("failed to get boot option: %w", err)
appConf: conf, // }
}
}
func (bc *BootController) setBootOption(w http.ResponseWriter, r *http.Request) (int, []byte, error) { // dat, err := json.Marshal(bootOption)
dat, err := io.ReadAll(r.Body) // if err != nil {
if err != nil { // return http.StatusInternalServerError, nil, fmt.Errorf("failed to serialize body")
return http.StatusInternalServerError, nil, fmt.Errorf("failed to read body: %w", err) // }
}
var payload setBootOptionPayload
if err := json.Unmarshal(dat, &payload); err != nil {
return http.StatusBadRequest, nil, fmt.Errorf("failed to parse body: %w", err)
}
clientID, err := uuid.Parse(payload.ClientID) // w.Header().Add("Content-Type", "application/json")
if err != nil { // return http.StatusOK, dat, nil
return http.StatusBadRequest, []byte("bad client ID"), fmt.Errorf("invalid format for client ID: %w", err) // }
}
optionID, err := uuid.Parse(payload.OptionID)
if err != nil {
return http.StatusBadRequest, []byte("bad option ID"), fmt.Errorf("invalid format for option ID: %w", err)
}
if err := bc.clientService.SetClientBootOption(clientID, optionID.String()); err != nil { // func (bc *BootController) setBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, error) {
if errors.Is(err, services.ErrUnknownClient) { // dat, err := io.ReadAll(r.Body)
return http.StatusNotFound, []byte("unknown client"), err // if err != nil {
} // return http.StatusInternalServerError, fmt.Errorf("failed to read body: %w", err)
if errors.Is(err, services.ErrUnknownBootOption) { // }
return http.StatusNotFound, []byte("unknown boot option"), err // var option string
} // if err := json.Unmarshal(dat, &option); err != nil {
return http.StatusInternalServerError, nil, fmt.Errorf("failed to set boot option for client: %w", err) // return http.StatusInternalServerError, fmt.Errorf("failed to parse body: %w", err)
} // }
if bc.appConf.HomeAssistantEnabled { // if err := bc.clientService.SetClientBootOption(clientIP, option); err != nil {
bc.l.Debug("Notifying HomeAssistant of change") // return http.StatusInternalServerError, fmt.Errorf("failed to set boot option for client: %w", err)
newConf, err := bc.clientService.GetClientConfig(clientID) // }
if err != nil {
bc.l.Errorf("Failed to get new config to send to HA: %s", err.Error())
} else {
if err := homeassistant.New(bc.appConf).SendBootOption(r.Context(), newConf.Name, newConf.Options[newConf.SelectedOption].Name); err != nil {
bc.l.Errorf("Failed to notify HA: %s", err.Error())
}
}
}
return http.StatusAccepted, nil, nil // return http.StatusAccepted, nil
} // }
func (bc *BootController) deleteClient(w http.ResponseWriter, r *http.Request) (int, []byte, error) { // func (bc *BootController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
dat, err := io.ReadAll(r.Body) // clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil { // if err != nil {
return http.StatusInternalServerError, nil, fmt.Errorf("failed to read body: %w", err) // bc.l.Errorf("Failed to read remote IP: %s", err.Error())
} // helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, bc.l)
var payload setBootOptionPayload // return
if err := json.Unmarshal(dat, &payload); err != nil { // }
return http.StatusBadRequest, nil, fmt.Errorf("failed to parse body: %w", err)
}
clientID, err := uuid.Parse(payload.ClientID) // var returncode int
if err != nil { // var content []byte
return http.StatusBadRequest, []byte("bad client ID"), fmt.Errorf("invalid format for client ID: %w", err)
}
if err := bc.clientService.DeleteClient(clientID); err != nil {
return http.StatusInternalServerError, []byte("failed to delete client"), fmt.Errorf("failed to delete client: %w", err)
}
return http.StatusOK, nil, nil // switch r.Method {
} // case http.MethodGet:
// returncode, content, err = bc.getBootOption(clientIP, w, r)
// case http.MethodPut:
// returncode, err = bc.setBootOption(clientIP, w, r)
// default:
// returncode = http.StatusMethodNotAllowed
// }
func (bc *BootController) ServeHTTP(w http.ResponseWriter, r *http.Request) { // if err != nil {
var returncode int // bc.l.Errorf("An error occured while handling boot request: %q", err.Error())
var content []byte // }
var err error // helpers.HandleResponse(w, r, returncode, content, bc.l)
// }
switch r.Method {
case http.MethodPut:
returncode, content, err = bc.setBootOption(w, r)
if err != nil {
bc.l.Errorf("Error setting boot option for client: %s", err.Error())
}
case http.MethodDelete:
returncode, content, err = bc.deleteClient(w, r)
if err != nil {
bc.l.Errorf("Error setting boot option for client: %s", err.Error())
}
default:
helpers.HandleResponse(w, r, http.StatusMethodNotAllowed, nil, bc.l)
return
}
helpers.HandleResponse(w, r, returncode, content, bc.l)
}

View file

@ -16,24 +16,17 @@ const EnrollRoute = "/enroll"
type newClientPayload struct { type newClientPayload struct {
ID string `json:"ID"` ID string `json:"ID"`
MulticastGroup string `json:"multicast_group"`
MulticastPort int `json:"multicast_port"`
} }
type EnrollController struct { type EnrollController struct {
clientService *services.ClientHandlerService clientService *services.ClientHandlerService
l *logrus.Logger l *logrus.Logger
multicastPort int
multicastGroup string
} }
func NewEnrollController(l *logrus.Logger, service *services.ClientHandlerService, mcastPort int, mcastGroup string) *EnrollController { func NewEnrollController(l *logrus.Logger, service *services.ClientHandlerService) *EnrollController {
return &EnrollController{ return &EnrollController{
clientService: service, clientService: service,
l: l, l: l,
multicastPort: mcastPort,
multicastGroup: mcastGroup,
} }
} }
@ -57,13 +50,12 @@ func (ec *EnrollController) enrollMachine(w http.ResponseWriter, r *http.Request
return http.StatusInternalServerError, nil, fmt.Errorf("failed to create client %w", err) return http.StatusInternalServerError, nil, fmt.Errorf("failed to create client %w", err)
} }
payload, err := json.Marshal(newClientPayload{ID: cltID.String(), MulticastGroup: ec.multicastGroup, MulticastPort: ec.multicastPort}) payload, err := json.Marshal(newClientPayload{ID: cltID.String()})
if err != nil { if err != nil {
return http.StatusInternalServerError, nil, fmt.Errorf("failed to serialize payload: %w", err) return http.StatusInternalServerError, nil, fmt.Errorf("failed to serialize payload: %w", err)
} }
ec.l.Infof("Added client") ec.l.Infof("Added client")
w.Header().Add("Content-Type", "application/json")
return http.StatusOK, payload, nil return http.StatusOK, payload, nil
} }

View file

@ -7,14 +7,9 @@ import (
const StaticRoute = "/static/" const StaticRoute = "/static/"
type StaticController struct { type StaticController struct {
staticDir string
}
func NewStaticController(staticDir string) *StaticController {
return &StaticController{staticDir: staticDir}
} }
func (sc *StaticController) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (sc *StaticController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fs := http.FileServer(http.Dir(sc.staticDir)) fs := http.FileServer(http.Dir("./static"))
http.StripPrefix(StaticRoute, fs).ServeHTTP(w, r) http.StripPrefix(StaticRoute, fs).ServeHTTP(w, r)
} }

View file

@ -7,7 +7,6 @@ import (
"io" "io"
"net/http" "net/http"
"path/filepath" "path/filepath"
"sort"
"git.faercol.me/faercol/http-boot-server/bootserver/helpers" "git.faercol.me/faercol/http-boot-server/bootserver/helpers"
"git.faercol.me/faercol/http-boot-server/bootserver/services" "git.faercol.me/faercol/http-boot-server/bootserver/services"
@ -36,19 +35,17 @@ type templateData struct {
type UIController struct { type UIController struct {
clientService *services.ClientHandlerService clientService *services.ClientHandlerService
l *logrus.Logger l *logrus.Logger
staticDir string
} }
func NewUIController(l *logrus.Logger, service *services.ClientHandlerService, staticDir string) *UIController { func NewUIController(l *logrus.Logger, service *services.ClientHandlerService) *UIController {
return &UIController{ return &UIController{
clientService: service, clientService: service,
l: l, l: l,
staticDir: staticDir,
} }
} }
func (uc *UIController) serveUI(w http.ResponseWriter, r *http.Request) (int, int, error) { func (uc *UIController) serveUI(w http.ResponseWriter, r *http.Request) (int, int, error) {
lp := filepath.Join(uc.staticDir, "templates", "index.html") lp := filepath.Join("templates", "index.html")
tmpl, err := template.ParseFiles(lp) tmpl, err := template.ParseFiles(lp)
if err != nil { if err != nil {
return http.StatusInternalServerError, -1, fmt.Errorf("failed to init template: %w", err) return http.StatusInternalServerError, -1, fmt.Errorf("failed to init template: %w", err)
@ -66,13 +63,10 @@ func (uc *UIController) serveUI(w http.ResponseWriter, r *http.Request) (int, in
for id, bo := range clt.Options { for id, bo := range clt.Options {
tplBO = append(tplBO, templateBootOption{ tplBO = append(tplBO, templateBootOption{
Name: bo.Name, Name: bo.Name,
Path: bo.DevicePath, Path: bo.Path,
ID: id, ID: id,
Selected: id == clt.SelectedOption, Selected: id == clt.SelectedOption,
}) })
sort.Slice(tplBO, func(i, j int) bool {
return tplBO[i].ID < tplBO[j].ID
})
} }
dat.Clients = append(dat.Clients, templateClient{ dat.Clients = append(dat.Clients, templateClient{
ID: clt.ID.String(), ID: clt.ID.String(),
@ -93,16 +87,10 @@ func (uc *UIController) serveUI(w http.ResponseWriter, r *http.Request) (int, in
} }
func (uc *UIController) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (uc *UIController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.RequestURI != "/" {
uc.l.Errorf("Unhandled route %q", r.RequestURI)
helpers.HandleResponse(w, r, http.StatusNotFound, nil, uc.l)
return
}
returncode, contentLength, err := uc.serveUI(w, r) returncode, contentLength, err := uc.serveUI(w, r)
if err != nil { if err != nil {
uc.l.Errorf("Error serving UI: %s", err.Error()) uc.l.Errorf("Error serving UI: %s", err.Error())
helpers.HandleResponse(w, r, returncode, nil, uc.l) helpers.HandleResponse(w, r, returncode, nil, uc.l)
} else {
helpers.AddToContext(r, returncode, contentLength)
} }
helpers.AddToContext(r, returncode, contentLength)
} }

View file

@ -1,80 +0,0 @@
package homeassistant
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"git.faercol.me/faercol/http-boot-server/bootserver/config"
"git.faercol.me/faercol/http-boot-server/bootserver/logger"
)
type Entity struct {
State string `json:"state"`
ID string `json:"-"`
Attributes map[string]string `json:"attributes,omitempty"`
}
func newBootOptionEntity(device, option string) Entity {
return Entity{
State: option,
ID: "httpboot." + device,
Attributes: nil,
}
}
type HomeAssistantExporter struct {
clt *http.Client
baseURL string
token string
}
func New(conf *config.AppConfig) *HomeAssistantExporter {
clt := http.Client{}
return &HomeAssistantExporter{
clt: &clt,
baseURL: conf.HomeAssistantHost,
token: conf.HomeAssistantToken,
}
}
func (e *HomeAssistantExporter) SendBootOption(ctx context.Context, device string, option string) error {
subCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
entity := newBootOptionEntity(device, option)
dat, err := json.Marshal(entity)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(subCtx, http.MethodPost, e.baseURL+"/api/states/"+entity.ID, bytes.NewBuffer(dat))
if err != nil {
return err
}
req.Header.Add("Authorization", "Bearer "+e.token)
resp, err := e.clt.Do(req)
if err != nil {
return err
}
switch resp.StatusCode {
case http.StatusOK:
logger.L.Debugf("Updated boot info for device %s to %s", device, option)
case http.StatusCreated:
logger.L.Debugf("Created boot info for device %s with value %s", device, option)
default:
respBod, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("unexpected returncode %d (%s)", resp.StatusCode, string(respBod))
}
return nil
}

View file

@ -8,7 +8,6 @@ import (
"time" "time"
"git.faercol.me/faercol/http-boot-server/bootserver/config" "git.faercol.me/faercol/http-boot-server/bootserver/config"
"git.faercol.me/faercol/http-boot-server/bootserver/homeassistant"
"git.faercol.me/faercol/http-boot-server/bootserver/logger" "git.faercol.me/faercol/http-boot-server/bootserver/logger"
"git.faercol.me/faercol/http-boot-server/bootserver/server" "git.faercol.me/faercol/http-boot-server/bootserver/server"
"git.faercol.me/faercol/http-boot-server/bootserver/services" "git.faercol.me/faercol/http-boot-server/bootserver/services"
@ -62,21 +61,6 @@ func main() {
logger.L.Fatalf("Failed to start UDP listener: %s", err.Error()) logger.L.Fatalf("Failed to start UDP listener: %s", err.Error())
} }
if conf.HomeAssistantEnabled {
logger.L.Info("Home assistant integration enabled, sending current configuration to the host")
haClt := homeassistant.New(conf)
cltSrv := services.NewClientHandlerService(conf.DataFilepath, logger.L)
clts, err := cltSrv.GetAllClientConfig()
if err != nil {
logger.L.Fatalf("Failed to get current clients from the storage: %s", err.Error())
}
for _, c := range clts {
if err := haClt.SendBootOption(context.Background(), c.Name, c.Options[c.SelectedOption].Name); err != nil {
logger.L.Errorf("Failed to send config to homeassistant: %s", err.Error())
}
}
}
go s.Run(mainCtx) go s.Run(mainCtx)
go listener.Run(mainCtx) go listener.Run(mainCtx)

View file

@ -67,11 +67,10 @@ func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) {
} }
service := services.NewClientHandlerService(appConf.DataFilepath, logger) service := services.NewClientHandlerService(appConf.DataFilepath, logger)
controllers := map[string]http.Handler{ controllers := map[string]http.Handler{
client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service, appConf.UDPPort, appConf.UPDMcastGroup), logger), client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service), logger),
client.ConfigRoute: middlewares.WithLogger(client.NewGetConfigController(logger, service, appConf), logger), client.ConfigRoute: middlewares.WithLogger(client.NewGetConfigController(logger, service, appConf), logger),
client.SetBootRoute: middlewares.WithLogger(client.NewBootController(logger, service, appConf), logger), ui.StaticRoute: &ui.StaticController{},
ui.StaticRoute: middlewares.WithLogger(ui.NewStaticController(appConf.StaticDir), logger), ui.UIRoute: middlewares.WithLogger(ui.NewUIController(logger, service), logger),
ui.UIRoute: middlewares.WithLogger(ui.NewUIController(logger, service, appConf.StaticDir), logger),
} }
m := http.NewServeMux() m := http.NewServeMux()
@ -87,7 +86,6 @@ func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) {
address: addr, address: addr,
clients: make(map[string]bootoption.Client), clients: make(map[string]bootoption.Client),
controllers: controllers, controllers: controllers,
ctx: context.TODO(),
}, nil }, nil
} }

View file

@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"sort"
"time" "time"
"git.faercol.me/faercol/http-boot-server/bootserver/bootoption" "git.faercol.me/faercol/http-boot-server/bootserver/bootoption"
@ -112,9 +111,6 @@ func (chs *ClientHandlerService) GetAllClientConfig() ([]*bootoption.Client, err
clt.ID = id clt.ID = id
clientList = append(clientList, clt) clientList = append(clientList, clt)
} }
sort.Slice(clientList, func(i, j int) bool {
return clientList[i].ID.String() < clientList[j].ID.String()
})
return clientList, nil return clientList, nil
} }
@ -156,20 +152,6 @@ func (chs *ClientHandlerService) GetClientSelectedBootOption(client uuid.UUID) (
} }
} }
func (chs *ClientHandlerService) DeleteClient(client uuid.UUID) error {
var err error
clients, err := chs.load()
if err != nil {
return fmt.Errorf("failed to load current config: %w", err)
}
delete(clients, client)
if err := chs.unload(clients); err != nil {
return fmt.Errorf("failed to save current config: %w", err)
}
return nil
}
func (chs *ClientHandlerService) SetClientBootOption(client uuid.UUID, option string) error { func (chs *ClientHandlerService) SetClientBootOption(client uuid.UUID, option string) error {
var err error var err error

View file

@ -1,20 +0,0 @@
function selectBootOption(clientID, bootID) {
const Http = new XMLHttpRequest;
var host = window.location.protocol + "//" + window.location.host;
const url = host + "/config/boot"
console.debug(`Sending request to ${url}`);
Http.open("PUT", url);
Http.setRequestHeader("Content-Type", "application/json");
const body = JSON.stringify({
client_id: clientID,
option_id: bootID,
});
Http.onload = () => {
if (Http.readyState === 4 && Http.status === 202) {
location.reload();
} else {
console.error(`Unexpected returncode ${Http.status}`)
}
};
Http.send(body);
}

View file

@ -1,108 +1,31 @@
:root {
/* Base colours */
--base: #1e1e2e;
--mantle: #181825;
--crust: #11111b;
/* Surface colours */
--surface0: #313244;
--surface1: #45475a;
--surface2: #585b70;
--overlay0: #6c7086;
--overlay1: #7f849c;
--overlay2: #9399b2;
/* Text colours */
--text: #cdd6f4;
--subtext0: #a6adc8;
--subtext1: #bac2de;
/* Main colours */
--rosewater: #f5e0dc;
--flamingo: #f2cdcd;
--pink: #f5c2e7;
--mauve: #cba6f7;
--red: #f38ba8;
--maroon: #eba0ac;
--peach: #fab387;
--yellow: #f9e2af;
--green: #a6e3a1;
--teal: #94e2d5;
--sky: #89dceb;
--sapphire: #74c7ec;
--blue: #89b4fa;
--lavender: #b4befe;
}
body { body {
background-color: var(--crust); background-color: black;
color: var(--text); color: white;
}
.page-content {
max-width: 1500px;
margin: auto;
}
.container {
border-radius: 20px;
background-color: var(--base);
padding: 5px 10px;
margin: 5px auto;
}
.header-grid {
display: grid;
grid-template-columns: 1fr 3fr;
align-items: stretch;
justify-items: stretch;
column-gap: 10px;
margin: 5px auto;
.container {
padding: 10px;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
}
.client-list-grid {
display: grid;
grid-template-columns: 1fr;
justify-items: stretch;
margin: 5px auto;
row-gap: 5px;
.container {
margin: 0;
}
}
.stat-container {
h2 {
margin: auto;
width: 100%;
vertical-align: middle;
}
} }
.main-container { .main-container {
border-radius: 5px;
background-color: #121212;
padding: 20px; padding: 20px;
} }
.title-container { .title-container {
color: var(--blue); border-radius: 5px 5px 0px 0px;
padding: 5px;
background-color: #1A3A5D;
}
.client-list-container {
border-radius: 0px 0px 5px 5px;
padding: 10px;
background-color: #232323;
}
h1 { .client-container {
margin: auto; border-radius: 5px;
padding: auto; padding: 4px;
vertical-align: middle; margin-top: 2px;
width: 100%; margin-bottom: 2px;
}
} }
.client-header { .client-header {
@ -113,7 +36,7 @@ body {
.client-uuid { .client-uuid {
font-size: smaller; font-size: smaller;
color: var(--subtext1); color: #9B9B9B;
} }
} }
@ -125,16 +48,9 @@ body {
.client-boot-option { .client-boot-option {
margin: 3px auto; margin: 3px auto;
padding: 8px 8px; padding: 8px 8px;
cursor: pointer;
&.selected { &.selected {
background-color: var(--overlay0); background-color: #3E4042;
cursor: default;
}
&:hover {
background-color: var(--overlay2);
--text-variant: #000000;
} }
} }
@ -145,7 +61,7 @@ body {
.boot-option-uuid { .boot-option-uuid {
font-size: smaller; font-size: smaller;
color: var(--subtext0); color: #9B9B9B;
} }
.boot-option-path { .boot-option-path {

View file

@ -5,36 +5,28 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>HTTP boot server</title> <title>HTTP boot server</title>
<link rel="stylesheet" href="/static/stylesheets/main.css"> <link rel="stylesheet" href="/static/stylesheets/main.css">
<script src="/static/scripts/index.js"></script>
</head> </head>
<body> <body>
<div class="page-content"> <div class="main-container">
<div class="header-grid"> <div class="title-container">
<div class="stat-container container">
<h2>{{len .Clients}} enrolled clients</h2>
</div>
<div class="title-container container">
<h1>HTTP boot server admin page</h1> <h1>HTTP boot server admin page</h1>
</div> </div>
</div> <div class="client-list-container">
<h2>Currently enrolled clients</h2>
<div class="client-list-grid">
{{range .Clients}} {{range .Clients}}
<div class="client-container container"> <div class="client-container">
<div class="client-header"> <div class="client-header">
<span class="client-name">{{.Name}}</span> <span class="client-name">{{.Name}}</span>
<span class="client-uuid">{{.ID}}</span> <span class="client-uuid">{{.ID}}</span>
</div> </div>
<div class="client-content"> <div class="client-content">
<div class="client-boot-options"> <div class="client-boot-options">
{{$cid := .ID}}{{range .BootOptions}} {{range .BootOptions}}
<div class="client-boot-option{{if .Selected}} selected{{end}}" <div class="client-boot-option{{if .Selected}} selected{{end}}">
onclick="selectBootOption('{{$cid}}', '{{.ID}}')">
<div> <div>
<span class="boot-option-name">{{.Name}}</span> <span class="boot-option-name">{{.Name}}</span>
<span class="boot-option-uuid">{{.ID}}</span> <span class="boot-option-uuid">{{.ID}}</span>

View file

@ -3,7 +3,6 @@ package udplistener
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@ -21,28 +20,14 @@ type udpMessage struct {
message bootprotocol.Message message bootprotocol.Message
} }
type discoveryPayload struct {
ManagementAddress string `json:"managementAddress"`
Version string `json:"version"`
}
func payloadFromConfig(conf config.AppConfig) discoveryPayload {
return discoveryPayload{
ManagementAddress: conf.PublicHost,
Version: "1",
}
}
type UDPListener struct { type UDPListener struct {
addr *net.UDPAddr addr *net.UDPAddr
laddr *net.UDPAddr
iface *net.Interface iface *net.Interface
l *net.UDPConn l *net.UDPConn
log *logrus.Logger log *logrus.Logger
ctx context.Context ctx context.Context
service *services.ClientHandlerService service *services.ClientHandlerService
cancel context.CancelFunc cancel context.CancelFunc
conf *config.AppConfig
} }
func New(conf *config.AppConfig, log *logrus.Logger) (*UDPListener, error) { func New(conf *config.AppConfig, log *logrus.Logger) (*UDPListener, error) {
@ -56,19 +41,12 @@ func New(conf *config.AppConfig, log *logrus.Logger) (*UDPListener, error) {
return nil, fmt.Errorf("failed to resolve interface name %s: %w", conf.UDPIface, err) return nil, fmt.Errorf("failed to resolve interface name %s: %w", conf.UDPIface, err)
} }
laddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("[%s%%%s]:0", conf.UDPSrcAddr, conf.UDPIface))
if err != nil {
return nil, fmt.Errorf("failed to resolve UDP source address: %w", err)
}
return &UDPListener{ return &UDPListener{
addr: addr, addr: addr,
iface: iface, iface: iface,
laddr: laddr,
ctx: context.TODO(), ctx: context.TODO(),
service: services.NewClientHandlerService(conf.DataFilepath, log), service: services.NewClientHandlerService(conf.DataFilepath, log),
log: log, log: log,
conf: conf,
}, nil }, nil
} }
@ -100,7 +78,7 @@ func (l *UDPListener) handleBootRequest(msg bootprotocol.Message, subLogger logr
requestLogger.Errorf("Failed to get config for client: %s", err.Error()) requestLogger.Errorf("Failed to get config for client: %s", err.Error())
return bootprotocol.Deny(msg.ID(), "unknown server error") return bootprotocol.Deny(msg.ID(), "unknown server error")
} }
return bootprotocol.Accept(msg.ID(), bootOption.DevicePath) return bootprotocol.Accept(msg.ID(), bootOption.Path)
} }
func (l *UDPListener) handleClient(msg *udpMessage) error { func (l *UDPListener) handleClient(msg *udpMessage) error {
@ -110,7 +88,7 @@ func (l *UDPListener) handleClient(msg *udpMessage) error {
response := l.handleBootRequest(msg.message, clientLogger) response := l.handleBootRequest(msg.message, clientLogger)
clientLogger.Debug("Dialing client for answer") clientLogger.Debug("Dialing client for answer")
con, err := net.DialUDP("udp", l.laddr, msg.sourceAddr) con, err := net.DialUDP("udp", nil, msg.sourceAddr)
if err != nil { if err != nil {
return fmt.Errorf("failed to dialed client: %w", err) return fmt.Errorf("failed to dialed client: %w", err)
} }
@ -132,33 +110,6 @@ func (l *UDPListener) handleClient(msg *udpMessage) error {
return nil return nil
} }
func (l *UDPListener) handleDiscovery(src *net.UDPAddr) error {
clientLogger := l.log.WithField("clientIP", src.IP)
clientLogger.Debug("Dialing client for answer")
con, err := net.DialUDP("udp", l.laddr, src)
if err != nil {
return fmt.Errorf("failed to dial client: %w", err)
}
defer con.Close()
clientLogger.Debug("Sending response to client")
response := payloadFromConfig(*l.conf)
dat, err := json.Marshal(response)
if err != nil {
return fmt.Errorf("failed to marshal response to json, %w", err)
}
n, err := con.Write(dat)
if err != nil {
return fmt.Errorf("failed to send response to client, %w", err)
}
if n != len(dat) {
return fmt.Errorf("failed to send the entire response to client (%d/%d)", n, len(dat))
}
return nil
}
func (l *UDPListener) listen() (*udpMessage, error) { func (l *UDPListener) listen() (*udpMessage, error) {
buffer := make([]byte, bufferLength) buffer := make([]byte, bufferLength)
n, source, err := l.l.ReadFromUDP(buffer) n, source, err := l.l.ReadFromUDP(buffer)
@ -180,7 +131,6 @@ func (l *UDPListener) listen() (*udpMessage, error) {
func (l *UDPListener) mainLoop() { func (l *UDPListener) mainLoop() {
msgChan := make(chan *udpMessage, 10) msgChan := make(chan *udpMessage, 10)
discoveryChan := make(chan *net.UDPAddr, 10)
errChan := make(chan error, 10) errChan := make(chan error, 10)
for { for {
@ -189,13 +139,9 @@ func (l *UDPListener) mainLoop() {
msg, err := l.listen() msg, err := l.listen()
if err != nil { if err != nil {
errChan <- fmt.Errorf("error while listening to UDP packets: %w", err) errChan <- fmt.Errorf("error while listening to UDP packets: %w", err)
} else {
if msg.message.Action() == bootprotocol.ActionDiscover {
discoveryChan <- msg.sourceAddr
} else { } else {
msgChan <- msg msgChan <- msg
} }
}
}() }()
l.log.Debug("Waiting for packets") l.log.Debug("Waiting for packets")
@ -212,10 +158,6 @@ func (l *UDPListener) mainLoop() {
if err := l.handleClient(msg); err != nil { if err := l.handleClient(msg); err != nil {
l.log.Errorf("Failed to handle message from client: %q", err.Error()) l.log.Errorf("Failed to handle message from client: %q", err.Error())
} }
case src := <-discoveryChan:
if err := l.handleDiscovery(src); err != nil {
l.log.Errorf("Failed to handle discovery message: %q", err.Error())
}
} }
} }