Compare commits

...

8 commits
main ... dev

Author SHA1 Message Date
b250a6b3c8 Allow sending correct public name in autodiscovery 2024-03-27 22:07:09 +01:00
b1e9a2099e allow setting path to static files in config 2024-03-24 13:37:40 +01:00
e730e40a4a Add discovery to protocol 2024-03-24 13:21:56 +01:00
19b863e5ca use catppuccin-mocha
Some checks failed
continuous-integration/drone/push Build is failing
2023-09-16 15:39:08 +02:00
0318d5caf4 Allow deleting clients
Some checks failed
continuous-integration/drone/push Build is failing
2023-09-16 13:52:12 +02:00
0a58ceefbc fix multicast config for server 2023-09-16 13:52:05 +02:00
ebfd2ea96c Use device path to describe boot apps 2023-09-16 13:51:01 +02:00
2ae7327f6b Improve web interface and allow setting boot option 2023-08-19 13:49:13 +02:00
13 changed files with 415 additions and 133 deletions

View file

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

View file

@ -6,6 +6,7 @@ import (
"encoding" "encoding"
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -16,11 +17,12 @@ 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"
@ -41,6 +43,8 @@ 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"
} }
@ -54,6 +58,8 @@ 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
} }
@ -140,9 +146,13 @@ 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, am.efiApp)), []byte(fmt.Sprintf("%s=%s", keyEfiApp, 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
@ -212,11 +222,30 @@ 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, found := bytes.Cut(dat, spaceByte) rawAction, content, _ := bytes.Cut(dat, spaceByte)
if !found {
return nil, ErrInvalidFormat
}
var message Message var message Message
action := newActionFromBytes(rawAction) action := newActionFromBytes(rawAction)
@ -227,12 +256,14 @@ 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 message: %w", err) return nil, fmt.Errorf("failed to parse %s message: %w", message.Action().String(), err)
} }
return message, nil return message, nil
} }

View file

@ -44,17 +44,21 @@ 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"`
} }
@ -62,12 +66,15 @@ 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 {
@ -93,11 +100,14 @@ 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
} }
@ -106,10 +116,12 @@ 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,85 +1,110 @@
package client package client
// import ( import (
// "encoding/json" "encoding/json"
// "fmt" "errors"
// "io" "fmt"
// "net" "io"
// "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/sirupsen/logrus" "github.com/google/uuid"
// ) "github.com/sirupsen/logrus"
)
// const BootRoute = "/boot" const SetBootRoute = "/config/boot"
// type BootController struct { type setBootOptionPayload struct {
// clientService *services.ClientHandlerService ClientID string `json:"client_id"`
// l *logrus.Logger OptionID string `json:"option_id"`
// } }
// func NewBootController(logger *logrus.Logger, service *services.ClientHandlerService) *BootController { type BootController struct {
// return &BootController{ clientService *services.ClientHandlerService
// clientService: service, l *logrus.Logger
// l: logger, }
// }
// }
// func (bc *BootController) getBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, []byte, error) { func NewBootController(logger *logrus.Logger, service *services.ClientHandlerService) *BootController {
// bootOption, err := bc.clientService.GetClientSelectedBootOption(clientIP) return &BootController{
// if err != nil { clientService: service,
// return http.StatusInternalServerError, nil, fmt.Errorf("failed to get boot option: %w", err) l: logger,
// } }
}
// dat, err := json.Marshal(bootOption) func (bc *BootController) setBootOption(w http.ResponseWriter, r *http.Request) (int, []byte, error) {
// if err != nil { dat, err := io.ReadAll(r.Body)
// return http.StatusInternalServerError, nil, fmt.Errorf("failed to serialize 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)
}
// w.Header().Add("Content-Type", "application/json") clientID, err := uuid.Parse(payload.ClientID)
// return http.StatusOK, dat, nil 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)
}
// func (bc *BootController) setBootOption(clientIP string, w http.ResponseWriter, r *http.Request) (int, error) { if err := bc.clientService.SetClientBootOption(clientID, optionID.String()); err != nil {
// dat, err := io.ReadAll(r.Body) if errors.Is(err, services.ErrUnknownClient) {
// if err != nil { return http.StatusNotFound, []byte("unknown client"), err
// return http.StatusInternalServerError, fmt.Errorf("failed to read body: %w", err) }
// } if errors.Is(err, services.ErrUnknownBootOption) {
// var option string return http.StatusNotFound, []byte("unknown boot option"), err
// if err := json.Unmarshal(dat, &option); err != nil { }
// return http.StatusInternalServerError, fmt.Errorf("failed to parse body: %w", err) return http.StatusInternalServerError, nil, fmt.Errorf("failed to set boot option for client: %w", err)
// } }
// if err := bc.clientService.SetClientBootOption(clientIP, option); err != nil { return http.StatusAccepted, nil, nil
// return http.StatusInternalServerError, fmt.Errorf("failed to set boot option for client: %w", err) }
// }
// 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) { clientID, err := uuid.Parse(payload.ClientID)
// clientIP, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil {
// if err != nil { return http.StatusBadRequest, []byte("bad client ID"), fmt.Errorf("invalid format for client ID: %w", err)
// bc.l.Errorf("Failed to read remote IP: %s", err.Error()) }
// helpers.HandleResponse(w, r, http.StatusInternalServerError, nil, bc.l) if err := bc.clientService.DeleteClient(clientID); err != nil {
// return return http.StatusInternalServerError, []byte("failed to delete client"), fmt.Errorf("failed to delete client: %w", err)
// } }
// var returncode int return http.StatusOK, nil, nil
// var content []byte }
// switch r.Method { func (bc *BootController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// case http.MethodGet: var returncode int
// returncode, content, err = bc.getBootOption(clientIP, w, r) var content []byte
// case http.MethodPut: var err error
// returncode, err = bc.setBootOption(clientIP, w, r)
// default:
// returncode = http.StatusMethodNotAllowed
// }
// if err != nil { switch r.Method {
// bc.l.Errorf("An error occured while handling boot request: %q", err.Error()) case http.MethodPut:
// } returncode, content, err = bc.setBootOption(w, r)
// helpers.HandleResponse(w, r, returncode, content, bc.l) 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,17 +16,24 @@ 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) *EnrollController { func NewEnrollController(l *logrus.Logger, service *services.ClientHandlerService, mcastPort int, mcastGroup string) *EnrollController {
return &EnrollController{ return &EnrollController{
clientService: service, clientService: service,
l: l, l: l,
multicastPort: mcastPort,
multicastGroup: mcastGroup,
} }
} }
@ -50,12 +57,13 @@ 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()}) payload, err := json.Marshal(newClientPayload{ID: cltID.String(), MulticastGroup: ec.multicastGroup, MulticastPort: ec.multicastPort})
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,9 +7,14 @@ 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("./static")) fs := http.FileServer(http.Dir(sc.staticDir))
http.StripPrefix(StaticRoute, fs).ServeHTTP(w, r) http.StripPrefix(StaticRoute, fs).ServeHTTP(w, r)
} }

View file

@ -7,6 +7,7 @@ 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"
@ -35,17 +36,19 @@ 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) *UIController { func NewUIController(l *logrus.Logger, service *services.ClientHandlerService, staticDir string) *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("templates", "index.html") lp := filepath.Join(uc.staticDir, "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)
@ -63,10 +66,13 @@ 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.Path, Path: bo.DevicePath,
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(),
@ -87,10 +93,16 @@ 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,10 +67,11 @@ 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), logger), client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service, appConf.UDPPort, appConf.UPDMcastGroup), logger),
client.ConfigRoute: middlewares.WithLogger(client.NewGetConfigController(logger, service, appConf), logger), client.ConfigRoute: middlewares.WithLogger(client.NewGetConfigController(logger, service, appConf), logger),
ui.StaticRoute: &ui.StaticController{}, client.SetBootRoute: middlewares.WithLogger(client.NewBootController(logger, service), logger),
ui.UIRoute: middlewares.WithLogger(ui.NewUIController(logger, service), logger), ui.StaticRoute: middlewares.WithLogger(ui.NewStaticController(appConf.StaticDir), logger),
ui.UIRoute: middlewares.WithLogger(ui.NewUIController(logger, service, appConf.StaticDir), logger),
} }
m := http.NewServeMux() m := http.NewServeMux()
@ -86,6 +87,7 @@ 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,6 +5,7 @@ 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"
@ -111,6 +112,9 @@ 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
} }
@ -152,6 +156,20 @@ 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

@ -0,0 +1,20 @@
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,31 +1,108 @@
: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: black; background-color: var(--crust);
color: white; 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;
}
} }
.main-container { .main-container {
border-radius: 5px;
background-color: #121212;
padding: 20px; padding: 20px;
} }
.title-container { .title-container {
border-radius: 5px 5px 0px 0px; color: var(--blue);
padding: 5px;
background-color: #1A3A5D;
}
.client-list-container {
border-radius: 0px 0px 5px 5px;
padding: 10px;
background-color: #232323;
}
.client-container { h1 {
border-radius: 5px; margin: auto;
padding: 4px; padding: auto;
margin-top: 2px; vertical-align: middle;
margin-bottom: 2px; width: 100%;
}
} }
.client-header { .client-header {
@ -36,7 +113,7 @@ body {
.client-uuid { .client-uuid {
font-size: smaller; font-size: smaller;
color: #9B9B9B; color: var(--subtext1);
} }
} }
@ -48,9 +125,16 @@ 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: #3E4042; background-color: var(--overlay0);
cursor: default;
}
&:hover {
background-color: var(--overlay2);
--text-variant: #000000;
} }
} }
@ -61,7 +145,7 @@ body {
.boot-option-uuid { .boot-option-uuid {
font-size: smaller; font-size: smaller;
color: #9B9B9B; color: var(--subtext0);
} }
.boot-option-path { .boot-option-path {

View file

@ -5,28 +5,36 @@
<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="main-container"> <div class="page-content">
<div class="title-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> <h1>HTTP boot server admin page</h1>
</div> </div>
<div class="client-list-container"> </div>
<h2>Currently enrolled clients</h2>
<div class="client-list-grid">
{{range .Clients}} {{range .Clients}}
<div class="client-container"> <div class="client-container 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">
{{range .BootOptions}} {{$cid := .ID}}{{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,6 +3,7 @@ package udplistener
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@ -20,14 +21,28 @@ 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) {
@ -41,12 +56,19 @@ 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
} }
@ -78,7 +100,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.Path) return bootprotocol.Accept(msg.ID(), bootOption.DevicePath)
} }
func (l *UDPListener) handleClient(msg *udpMessage) error { func (l *UDPListener) handleClient(msg *udpMessage) error {
@ -88,7 +110,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", nil, msg.sourceAddr) con, err := net.DialUDP("udp", l.laddr, 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)
} }
@ -110,6 +132,33 @@ 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)
@ -131,6 +180,7 @@ 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 {
@ -139,9 +189,13 @@ 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")
@ -158,6 +212,10 @@ 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())
}
} }
} }