Add udp listener
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Melora Hugues 2023-07-29 21:23:36 +02:00
parent 576c78e6dd
commit 0b6bbce7d5
9 changed files with 246 additions and 84 deletions

View file

@ -3,6 +3,9 @@
build:
go build -o build/
buildarm:
env GOOS=linux GOARCH=arm64 go build -o build/bootserver_arm
test:
go test -v ./...

View file

@ -1,5 +1,7 @@
package bootoption
import "github.com/google/uuid"
type EFIApp struct {
Name string `json:"name"`
Description string `json:"description"`
@ -7,6 +9,7 @@ type EFIApp struct {
}
type Client struct {
ID uuid.UUID
IP string `json:"ip"`
Name string `json:"name"`
Options []EFIApp `json:"options"`

View file

@ -64,6 +64,7 @@ type Message interface {
encoding.BinaryMarshaler
Action() Action
ID() uuid.UUID
String() string
}
type requestMessage struct {
@ -103,6 +104,10 @@ func (rm *requestMessage) ID() uuid.UUID {
return rm.id
}
func (rm *requestMessage) String() string {
return fmt.Sprintf("%s from %s", ActionRequest.String(), rm.ID().String())
}
type acceptMessage struct {
id uuid.UUID
efiApp string
@ -151,6 +156,10 @@ func (am *acceptMessage) ID() uuid.UUID {
return am.id
}
func (am *acceptMessage) String() string {
return fmt.Sprintf("%s from %s, app %s", ActionAccept.String(), am.ID().String(), am.efiApp)
}
type denyMessage struct {
id uuid.UUID
reason string
@ -199,6 +208,10 @@ func (dm *denyMessage) ID() uuid.UUID {
return dm.id
}
func (dm *denyMessage) String() string {
return fmt.Sprintf("%s from %s, reason %q", ActionDeny.String(), dm.ID().String(), dm.reason)
}
func MessageFromBytes(dat []byte) (Message, error) {
rawAction, content, found := bytes.Cut(dat, spaceByte)
if !found {

View file

@ -48,14 +48,22 @@ type jsonConf struct {
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"`
} `json:"boot_provider"`
}
type AppConfig struct {
LogLevel logrus.Level
ServerMode ListeningMode
Host string
Port int
SockPath string
LogLevel logrus.Level
ServerMode ListeningMode
Host string
Port int
SockPath string
UPDMcastGroup string
UDPPort int
UDPIface string
}
func parseLevel(lvlStr string) logrus.Level {
@ -82,14 +90,20 @@ func (ac *AppConfig) UnmarshalJSON(data []byte) error {
ac.SockPath = jsonConf.Server.SockPath
ac.Host = jsonConf.Server.Host
ac.Port = jsonConf.Server.Port
ac.UPDMcastGroup = jsonConf.BootProvider.McastGroup
ac.UDPIface = jsonConf.BootProvider.Iface
ac.UDPPort = jsonConf.BootProvider.Port
return nil
}
var defaultConfig AppConfig = AppConfig{
LogLevel: logrus.InfoLevel,
ServerMode: ModeNet,
Host: "0.0.0.0",
Port: 5000,
LogLevel: logrus.InfoLevel,
ServerMode: ModeNet,
Host: "0.0.0.0",
Port: 5000,
UPDMcastGroup: "ff02::abcd:1234",
UDPPort: 42,
UDPIface: "eth0",
}
func New(filepath string) (*AppConfig, error) {

View file

@ -1,85 +1,85 @@
package client
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
// import (
// "encoding/json"
// "fmt"
// "io"
// "net"
// "net/http"
"git.faercol.me/faercol/http-boot-server/bootserver/helpers"
"git.faercol.me/faercol/http-boot-server/bootserver/services"
"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 BootRoute = "/boot"
// const BootRoute = "/boot"
type BootController struct {
clientService *services.ClientHandlerService
l *logrus.Logger
}
// type BootController struct {
// clientService *services.ClientHandlerService
// 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) *BootController {
// return &BootController{
// clientService: service,
// l: logger,
// }
// }
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) 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)
// }
dat, err := json.Marshal(bootOption)
if err != nil {
return http.StatusInternalServerError, nil, fmt.Errorf("failed to serialize body")
}
// dat, err := json.Marshal(bootOption)
// if err != nil {
// return http.StatusInternalServerError, nil, fmt.Errorf("failed to serialize body")
// }
w.Header().Add("Content-Type", "application/json")
return http.StatusOK, dat, nil
}
// w.Header().Add("Content-Type", "application/json")
// return http.StatusOK, dat, nil
// }
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)
}
// 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 err := bc.clientService.SetClientBootOption(clientIP, option); err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to set boot option for client: %w", err)
}
// 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
}
// return http.StatusAccepted, nil
// }
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
}
// 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
// }
var returncode int
var content []byte
// var returncode int
// var content []byte
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
}
// 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
// }
if err != nil {
bc.l.Errorf("An error occured while handling boot request: %q", err.Error())
}
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

@ -10,6 +10,7 @@ import (
"git.faercol.me/faercol/http-boot-server/bootserver/config"
"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/udplistener"
)
const stopTimeout = 10 * time.Second
@ -47,7 +48,17 @@ func main() {
logger.L.Fatalf("Failed to initialize server: %s", err.Error())
}
logger.L.Info("Initializing UDP listener")
listener, err := udplistener.New(conf.UDPIface, conf.UPDMcastGroup, conf.UDPPort, logger.L)
if err != nil {
logger.L.Fatalf("Failed to initialize UDP listener: %s", err.Error())
}
if err := listener.Init(); err != nil {
logger.L.Fatalf("Failed to start UDP listener: %s", err.Error())
}
go s.Run(mainCtx)
go listener.Run(mainCtx)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)

View file

@ -66,7 +66,7 @@ func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) {
}
service := services.NewClientHandlerService()
controllers := map[string]http.Handler{
client.BootRoute: middlewares.WithLogger(client.NewBootController(logger, service), logger),
// client.BootRoute: middlewares.WithLogger(client.NewBootController(logger, service), logger),
client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger, service), logger),
}

View file

@ -4,6 +4,7 @@ import (
"errors"
"git.faercol.me/faercol/http-boot-server/bootserver/bootoption"
"github.com/google/uuid"
)
var ErrUnknownClient = errors.New("unknown client")
@ -11,20 +12,20 @@ var ErrUnselectedBootOption = errors.New("unselected boot option")
var ErrUnknownBootOption = errors.New("unknown boot option")
type ClientHandlerService struct {
clients map[string]*bootoption.Client
clients map[uuid.UUID]*bootoption.Client
}
func NewClientHandlerService() *ClientHandlerService {
return &ClientHandlerService{
clients: make(map[string]*bootoption.Client),
clients: make(map[uuid.UUID]*bootoption.Client),
}
}
func (chs *ClientHandlerService) AddClient(client *bootoption.Client) {
chs.clients[client.IP] = client
chs.clients[client.ID] = client
}
func (chs *ClientHandlerService) GetClientSelectedBootOption(client string) (*bootoption.EFIApp, error) {
func (chs *ClientHandlerService) GetClientSelectedBootOption(client uuid.UUID) (*bootoption.EFIApp, error) {
clientDetails, ok := chs.clients[client]
if !ok {
return nil, ErrUnknownClient
@ -42,7 +43,7 @@ func (chs *ClientHandlerService) GetClientSelectedBootOption(client string) (*bo
return nil, ErrUnknownBootOption
}
func (chs *ClientHandlerService) SetClientBootOption(client, option string) error {
func (chs *ClientHandlerService) SetClientBootOption(client uuid.UUID, option string) error {
clientDetails, ok := chs.clients[client]
if !ok {
return ErrUnknownClient

View file

@ -0,0 +1,117 @@
package udplistener
import (
"bytes"
"context"
"fmt"
"net"
"git.faercol.me/faercol/http-boot-server/bootserver/bootprotocol"
"git.faercol.me/faercol/http-boot-server/bootserver/services"
"github.com/sirupsen/logrus"
)
const bufferLength = 2048
type udpMessage struct {
sourceAddr *net.UDPAddr
message bootprotocol.Message
}
type UDPListener struct {
addr *net.UDPAddr
iface *net.Interface
l *net.UDPConn
log *logrus.Logger
ctx context.Context
service *services.ClientHandlerService
cancel context.CancelFunc
}
func New(ifaceName, multicastGroup string, port int, log *logrus.Logger) (*UDPListener, error) {
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("[%s]:%d", multicastGroup, port))
if err != nil {
return nil, fmt.Errorf("failed to resolve UDP address: %w", err)
}
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return nil, fmt.Errorf("failed to resolve interface name %s: %w", ifaceName, err)
}
return &UDPListener{
addr: addr,
iface: iface,
ctx: context.TODO(),
log: log,
}, nil
}
func (l *UDPListener) Init() error {
l.log.Debugf("Creating listener on address %s, iface %s", l.addr.String(), l.iface.Name)
listener, err := net.ListenMulticastUDP("udp", l.iface, l.addr)
if err != nil {
return fmt.Errorf("failed to init listener: %w", err)
}
l.l = listener
return nil
}
func (l *UDPListener) listen() (*udpMessage, error) {
buffer := make([]byte, bufferLength)
n, source, err := l.l.ReadFromUDP(buffer)
if err != nil {
return nil, fmt.Errorf("failed to read UDP packet: %w", err)
}
if n > bufferLength {
return nil, fmt.Errorf("UDP packet too big (%d/%d)", n, bufferLength)
}
parsedMsg, err := bootprotocol.MessageFromBytes(bytes.Trim(buffer, "\x00"))
if err != nil {
return nil, fmt.Errorf("failed to parse message: %w", err)
}
return &udpMessage{sourceAddr: source, message: parsedMsg}, nil
}
func (l *UDPListener) mainLoop() {
msgChan := make(chan *udpMessage, 10)
errChan := make(chan error, 10)
for {
go func() {
msg, err := l.listen()
if err != nil {
errChan <- fmt.Errorf("error while listening to UDP packets: %w", err)
} else {
msgChan <- msg
}
}()
l.log.Debug("Waiting for packets")
select {
case <-l.ctx.Done():
if err := l.l.Close(); err != nil {
l.log.Errorf("Error closing UDP listener: %s", err.Error())
}
return
case err := <-errChan:
l.log.Error(err)
case msg := <-msgChan:
l.log.Infof("Request from %s: %q", msg.sourceAddr.String(), msg.message.String())
}
}
}
func (l *UDPListener) Run(ctx context.Context) {
l.ctx, l.cancel = context.WithCancel(ctx)
l.mainLoop()
}
func (l *UDPListener) Cancel() {
l.cancel()
}