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

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

View file

@ -6,7 +6,6 @@ import (
"encoding"
"errors"
"fmt"
"strings"
"github.com/google/uuid"
)
@ -17,12 +16,11 @@ const (
ActionRequest Action = iota
ActionAccept
ActionDeny
ActionDiscover
ActionUnknown
)
var spaceByte = []byte(" ")
var commaByte = []byte(";")
var commaByte = []byte(",")
const (
keyID = "id"
@ -43,8 +41,6 @@ func (a Action) String() string {
return "BOOT_DENY"
case ActionRequest:
return "BOOT_REQUEST"
case ActionDiscover:
return "BOOT_DISCOVER"
default:
return "unknown"
}
@ -58,8 +54,6 @@ func newActionFromBytes(raw []byte) Action {
return ActionDeny
case "BOOT_REQUEST":
return ActionRequest
case "BOOT_DISCOVER":
return ActionDiscover
default:
return ActionUnknown
}
@ -146,13 +140,9 @@ func (am *acceptMessage) UnmarshalBinary(data []byte) error {
func (am *acceptMessage) MarshalBinary() (data []byte, err error) {
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{
[]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)
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)
}
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) {
rawAction, content, _ := bytes.Cut(dat, spaceByte)
rawAction, content, found := bytes.Cut(dat, spaceByte)
if !found {
return nil, ErrInvalidFormat
}
var message Message
action := newActionFromBytes(rawAction)
@ -256,14 +227,12 @@ func MessageFromBytes(dat []byte) (Message, error) {
message = &acceptMessage{}
case ActionDeny:
message = &denyMessage{}
case ActionDiscover:
message = &discoverMessage{}
default:
return nil, ErrUnknownAction
}
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
}

View file

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

View file

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

View file

@ -15,25 +15,18 @@ import (
const EnrollRoute = "/enroll"
type newClientPayload struct {
ID string `json:"ID"`
MulticastGroup string `json:"multicast_group"`
MulticastPort int `json:"multicast_port"`
ID string `json:"ID"`
}
type EnrollController struct {
clientService *services.ClientHandlerService
l *logrus.Logger
multicastPort int
multicastGroup string
clientService *services.ClientHandlerService
l *logrus.Logger
}
func NewEnrollController(l *logrus.Logger, service *services.ClientHandlerService, mcastPort int, mcastGroup string) *EnrollController {
func NewEnrollController(l *logrus.Logger, service *services.ClientHandlerService) *EnrollController {
return &EnrollController{
clientService: service,
l: l,
multicastPort: mcastPort,
multicastGroup: mcastGroup,
clientService: service,
l: l,
}
}
@ -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)
}
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 {
return http.StatusInternalServerError, nil, fmt.Errorf("failed to serialize payload: %w", err)
}
ec.l.Infof("Added client")
w.Header().Add("Content-Type", "application/json")
return http.StatusOK, payload, nil
}

View file

@ -7,14 +7,9 @@ import (
const StaticRoute = "/static/"
type StaticController struct {
staticDir string
}
func NewStaticController(staticDir string) *StaticController {
return &StaticController{staticDir: staticDir}
}
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)
}

View file

@ -7,7 +7,6 @@ import (
"io"
"net/http"
"path/filepath"
"sort"
"git.faercol.me/faercol/http-boot-server/bootserver/helpers"
"git.faercol.me/faercol/http-boot-server/bootserver/services"
@ -36,19 +35,17 @@ type templateData struct {
type UIController struct {
clientService *services.ClientHandlerService
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{
clientService: service,
l: l,
staticDir: staticDir,
}
}
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)
if err != nil {
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 {
tplBO = append(tplBO, templateBootOption{
Name: bo.Name,
Path: bo.DevicePath,
Path: bo.Path,
ID: id,
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{
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) {
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)
if err != nil {
uc.l.Errorf("Error serving UI: %s", err.Error())
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"
"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/server"
"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())
}
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 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)
controllers := map[string]http.Handler{
client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service, appConf.UDPPort, appConf.UPDMcastGroup), logger),
client.ConfigRoute: middlewares.WithLogger(client.NewGetConfigController(logger, service, appConf), logger),
client.SetBootRoute: middlewares.WithLogger(client.NewBootController(logger, service, appConf), logger),
ui.StaticRoute: middlewares.WithLogger(ui.NewStaticController(appConf.StaticDir), logger),
ui.UIRoute: middlewares.WithLogger(ui.NewUIController(logger, service, appConf.StaticDir), logger),
client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service), logger),
client.ConfigRoute: middlewares.WithLogger(client.NewGetConfigController(logger, service, appConf), logger),
ui.StaticRoute: &ui.StaticController{},
ui.UIRoute: middlewares.WithLogger(ui.NewUIController(logger, service), logger),
}
m := http.NewServeMux()
@ -87,7 +86,6 @@ func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) {
address: addr,
clients: make(map[string]bootoption.Client),
controllers: controllers,
ctx: context.TODO(),
}, nil
}

View file

@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"os"
"sort"
"time"
"git.faercol.me/faercol/http-boot-server/bootserver/bootoption"
@ -112,9 +111,6 @@ func (chs *ClientHandlerService) GetAllClientConfig() ([]*bootoption.Client, err
clt.ID = id
clientList = append(clientList, clt)
}
sort.Slice(clientList, func(i, j int) bool {
return clientList[i].ID.String() < clientList[j].ID.String()
})
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 {
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 {
background-color: var(--crust);
color: var(--text);
}
.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;
}
background-color: black;
color: white;
}
.main-container {
border-radius: 5px;
background-color: #121212;
padding: 20px;
}
.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 {
margin: auto;
padding: auto;
vertical-align: middle;
width: 100%;
}
.client-container {
border-radius: 5px;
padding: 4px;
margin-top: 2px;
margin-bottom: 2px;
}
.client-header {
@ -113,7 +36,7 @@ body {
.client-uuid {
font-size: smaller;
color: var(--subtext1);
color: #9B9B9B;
}
}
@ -125,16 +48,9 @@ body {
.client-boot-option {
margin: 3px auto;
padding: 8px 8px;
cursor: pointer;
&.selected {
background-color: var(--overlay0);
cursor: default;
}
&:hover {
background-color: var(--overlay2);
--text-variant: #000000;
background-color: #3E4042;
}
}
@ -145,7 +61,7 @@ body {
.boot-option-uuid {
font-size: smaller;
color: var(--subtext0);
color: #9B9B9B;
}
.boot-option-path {

View file

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

View file

@ -3,7 +3,6 @@ package udplistener
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net"
@ -21,28 +20,14 @@ type udpMessage struct {
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 {
addr *net.UDPAddr
laddr *net.UDPAddr
iface *net.Interface
l *net.UDPConn
log *logrus.Logger
ctx context.Context
service *services.ClientHandlerService
cancel context.CancelFunc
conf *config.AppConfig
}
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)
}
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{
addr: addr,
iface: iface,
laddr: laddr,
ctx: context.TODO(),
service: services.NewClientHandlerService(conf.DataFilepath, log),
log: log,
conf: conf,
}, 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())
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 {
@ -110,7 +88,7 @@ func (l *UDPListener) handleClient(msg *udpMessage) error {
response := l.handleBootRequest(msg.message, clientLogger)
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 {
return fmt.Errorf("failed to dialed client: %w", err)
}
@ -132,33 +110,6 @@ func (l *UDPListener) handleClient(msg *udpMessage) error {
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) {
buffer := make([]byte, bufferLength)
n, source, err := l.l.ReadFromUDP(buffer)
@ -180,7 +131,6 @@ func (l *UDPListener) listen() (*udpMessage, error) {
func (l *UDPListener) mainLoop() {
msgChan := make(chan *udpMessage, 10)
discoveryChan := make(chan *net.UDPAddr, 10)
errChan := make(chan error, 10)
for {
@ -190,11 +140,7 @@ func (l *UDPListener) mainLoop() {
if err != nil {
errChan <- fmt.Errorf("error while listening to UDP packets: %w", err)
} else {
if msg.message.Action() == bootprotocol.ActionDiscover {
discoveryChan <- msg.sourceAddr
} else {
msgChan <- msg
}
msgChan <- msg
}
}()
@ -212,10 +158,6 @@ func (l *UDPListener) mainLoop() {
if err := l.handleClient(msg); err != nil {
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())
}
}
}