Compare commits

..

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

13 changed files with 133 additions and 415 deletions

View file

@ -3,8 +3,9 @@ package bootoption
import "github.com/google/uuid" 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

@ -43,22 +43,18 @@ type jsonConf struct {
Level string `json:"level"` Level string `json:"level"`
} `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"`
} }
@ -66,15 +62,12 @@ 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
} }
func parseLevel(lvlStr string) logrus.Level { func parseLevel(lvlStr string) logrus.Level {
@ -100,14 +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
return nil return nil
} }
@ -116,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,110 +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/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"
"github.com/google/uuid" // "github.com/sirupsen/logrus"
"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{
l *logrus.Logger // clientService: service,
} // l: logger,
// }
// }
func NewBootController(logger *logrus.Logger, service *services.ClientHandlerService) *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)
} // }
}
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)
} // }
return http.StatusAccepted, nil, nil // if err := bc.clientService.SetClientBootOption(clientIP, option); err != nil {
} // return http.StatusInternalServerError, fmt.Errorf("failed to set boot option for client: %w", err)
// }
func (bc *BootController) deleteClient(w http.ResponseWriter, r *http.Request) (int, []byte, error) { // return http.StatusAccepted, nil
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)
}
clientID, err := uuid.Parse(payload.ClientID) // func (bc *BootController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil { // clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
return http.StatusBadRequest, []byte("bad client ID"), fmt.Errorf("invalid format for client ID: %w", err) // if err != nil {
} // bc.l.Errorf("Failed to read remote IP: %s", err.Error())
if err := bc.clientService.DeleteClient(clientID); err != nil { // helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, bc.l)
return http.StatusInternalServerError, []byte("failed to delete client"), fmt.Errorf("failed to delete client: %w", err) // return
} // }
return http.StatusOK, nil, nil // var returncode int
} // var content []byte
func (bc *BootController) ServeHTTP(w http.ResponseWriter, r *http.Request) { // switch r.Method {
var returncode int // case http.MethodGet:
var content []byte // returncode, content, err = bc.getBootOption(clientIP, w, r)
var err error // case http.MethodPut:
// returncode, err = bc.setBootOption(clientIP, w, r)
// default:
// returncode = http.StatusMethodNotAllowed
// }
switch r.Method { // if err != nil {
case http.MethodPut: // bc.l.Errorf("An error occured while handling boot request: %q", err.Error())
returncode, content, err = bc.setBootOption(w, r) // }
if err != nil { // helpers.HandleResponse(w, r, returncode, content, bc.l)
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

@ -15,25 +15,18 @@ import (
const EnrollRoute = "/enroll" 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

@ -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), 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="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="title-container">
<h1>HTTP boot server admin page</h1>
</div> </div>
<div class="client-list-grid"> <div class="client-list-container">
<h2>Currently enrolled clients</h2>
{{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 {
@ -190,11 +140,7 @@ func (l *UDPListener) mainLoop() {
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 { } else {
if msg.message.Action() == bootprotocol.ActionDiscover { msgChan <- msg
discoveryChan <- msg.sourceAddr
} else {
msgChan <- msg
}
} }
}() }()
@ -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())
}
} }
} }