Add first routes and log middleware handling

This commit is contained in:
Melora Hugues 2023-07-24 22:56:10 +02:00
parent b8bb280d0a
commit 9f39de9bca
9 changed files with 341 additions and 22 deletions

View file

@ -0,0 +1,14 @@
package bootoption
type EFIApp struct {
Name string `json:"name"`
Description string `json:"description"`
Path string `json:"path"`
}
type Client struct {
IP string `json:"ip"`
Name string `json:"name"`
Options []EFIApp `json:"options"`
SelectedOption string `json:"selected_option"`
}

View file

@ -0,0 +1,85 @@
package client
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"
)
const BootRoute = "/boot"
type BootController struct {
clientService *services.ClientHandlerService
l *logrus.Logger
}
func NewBootController(logger *logrus.Logger) *BootController {
return &BootController{
clientService: services.NewClientHandlerService(),
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)
}
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
}
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)
}
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
}
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
}
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

@ -0,0 +1,61 @@
package client
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"git.faercol.me/faercol/http-boot-server/bootserver/bootoption"
"git.faercol.me/faercol/http-boot-server/bootserver/helpers"
"git.faercol.me/faercol/http-boot-server/bootserver/services"
"github.com/sirupsen/logrus"
)
const EnrollRoute = "/enroll"
type EnrollController struct {
clientService *services.ClientHandlerService
l *logrus.Logger
}
func NewEnrollController(l *logrus.Logger) *EnrollController {
return &EnrollController{
clientService: services.NewClientHandlerService(),
l: l,
}
}
func (ec *EnrollController) enrollMachine(w http.ResponseWriter, r *http.Request) (int, error) {
if r.Method != http.MethodPost {
return http.StatusMethodNotAllowed, nil
}
clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to read remote IP: %w", err)
}
dat, err := io.ReadAll(r.Body)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to read body: %w", err)
}
var client bootoption.Client
if err := json.Unmarshal(dat, &client); err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to parse body: %w", err)
}
client.IP = clientIP
ec.clientService.AddClient(&client)
ec.l.Infof("Added client %s", clientIP)
return http.StatusAccepted, nil
}
func (ec *EnrollController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
returncode, err := ec.enrollMachine(w, r)
if err != nil {
ec.l.Errorf("Error handling client enrollement: %s", err.Error())
}
helpers.HandleResponse(w, r, returncode, nil, ec.l)
}

View file

@ -0,0 +1,31 @@
package helpers
import (
"context"
"net/http"
"github.com/sirupsen/logrus"
)
type ContextKey string
const ResponseInfoKey ContextKey = "response_info"
type ResponseInfo struct {
ReturnCode int
ContentLength int
}
func HandleResponse(w http.ResponseWriter, r *http.Request, returncode int, content []byte, l *logrus.Logger) {
w.WriteHeader(returncode)
n, err := w.Write(content)
if err != nil {
l.Errorf("Failed to write content to response: %q", err.Error())
}
if n != len(content) {
l.Errorf("Failed to write the entire response (%d/%d)", n, len(content))
}
contextedReq := r.WithContext(context.WithValue(r.Context(), ResponseInfoKey, ResponseInfo{ReturnCode: returncode, ContentLength: len(content)}))
*r = *contextedReq
}

View file

@ -7,4 +7,7 @@ var L *logrus.Logger
func Init(level logrus.Level) { func Init(level logrus.Level) {
L = logrus.New() L = logrus.New()
L.SetLevel(level) L.SetLevel(level)
L.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
})
} }

View file

@ -0,0 +1,39 @@
package middlewares
import (
"net"
"net/http"
"time"
"git.faercol.me/faercol/http-boot-server/bootserver/helpers"
"github.com/sirupsen/logrus"
)
var defaultResponseInfo = helpers.ResponseInfo{
ReturnCode: -1,
ContentLength: -1,
}
type LoggerMiddleware struct {
l *logrus.Logger
}
func (lm *LoggerMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
responseInfo, ok := r.Context().Value(helpers.ResponseInfoKey).(helpers.ResponseInfo)
if !ok {
lm.l.Errorf("Failed to read response info from context, got %v", r.Context().Value("response_info"))
responseInfo = defaultResponseInfo
}
method := r.Method
route := r.RequestURI
currentTime := time.Now().UTC()
httpVersion := r.Proto
clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
lm.l.Errorf("Failed to read remote IP: %s", err.Error())
clientIP = "unknown"
}
lm.l.Infof(`%s - [%v] "%s %s %s" %d %d`, clientIP, currentTime, method, route, httpVersion, responseInfo.ReturnCode, responseInfo.ContentLength)
}

View file

@ -0,0 +1,23 @@
package middlewares
import (
"net/http"
"github.com/sirupsen/logrus"
)
type MiddlewareChains struct {
handlers []http.Handler
}
func (mc *MiddlewareChains) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
for _, h := range mc.handlers {
h.ServeHTTP(rw, r)
}
}
func WithLogger(handler http.Handler, l *logrus.Logger) *MiddlewareChains {
return &MiddlewareChains{
handlers: []http.Handler{handler, &LoggerMiddleware{l}},
}
}

View file

@ -8,19 +8,24 @@ import (
"net/http" "net/http"
"os" "os"
"git.faercol.me/faercol/http-boot-server/bootserver/bootoption"
"git.faercol.me/faercol/http-boot-server/bootserver/config" "git.faercol.me/faercol/http-boot-server/bootserver/config"
"git.faercol.me/faercol/http-boot-server/bootserver/controllers/client"
"git.faercol.me/faercol/http-boot-server/bootserver/middlewares"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
type Server struct { type Server struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
httpSrv *http.Server httpSrv *http.Server
listener net.Listener listener net.Listener
serverMode config.ListeningMode serverMode config.ListeningMode
address string address string
handler *http.ServeMux handler *http.ServeMux
l *logrus.Logger l *logrus.Logger
clients map[string]bootoption.Client
controllers map[string]http.Handler
} }
func newUnixListener(sockPath string) (net.Listener, error) { func newUnixListener(sockPath string) (net.Listener, error) {
@ -59,6 +64,11 @@ func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) {
panic(fmt.Errorf("unexpected listening mode %v", appConf.ServerMode)) panic(fmt.Errorf("unexpected listening mode %v", appConf.ServerMode))
} }
controllers := map[string]http.Handler{
client.BootRoute: middlewares.WithLogger(client.NewBootController(logger), logger),
client.EnrollRoute: middlewares.WithLogger(client.NewEnrollController(logger), logger),
}
m := http.NewServeMux() m := http.NewServeMux()
return &Server{ return &Server{
@ -66,15 +76,19 @@ func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) {
httpSrv: &http.Server{ httpSrv: &http.Server{
Handler: m, Handler: m,
}, },
listener: listener, listener: listener,
l: logger, l: logger,
serverMode: appConf.ServerMode, serverMode: appConf.ServerMode,
address: addr, address: addr,
clients: make(map[string]bootoption.Client),
controllers: controllers,
}, nil }, nil
} }
func (s *Server) initMux() { func (s *Server) initMux() {
s.handler.HandleFunc("/", s.statusHandler) for r, c := range s.controllers {
s.handler.Handle(r, c)
}
} }
func (s *Server) Run(ctx context.Context) { func (s *Server) Run(ctx context.Context) {
@ -96,12 +110,3 @@ func (s *Server) Run(ctx context.Context) {
func (s *Server) Done() <-chan struct{} { func (s *Server) Done() <-chan struct{} {
return s.ctx.Done() return s.ctx.Done()
} }
func (s *Server) statusHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.Write([]byte("Hello world!"))
}

View file

@ -0,0 +1,58 @@
package services
import (
"errors"
"git.faercol.me/faercol/http-boot-server/bootserver/bootoption"
)
var ErrUnknownClient = errors.New("unknown client")
var ErrUnselectedBootOption = errors.New("unselected boot option")
var ErrUnknownBootOption = errors.New("unknown boot option")
type ClientHandlerService struct {
clients map[string]*bootoption.Client
}
func NewClientHandlerService() *ClientHandlerService {
return &ClientHandlerService{
clients: make(map[string]*bootoption.Client),
}
}
func (chs *ClientHandlerService) AddClient(client *bootoption.Client) {
chs.clients[client.IP] = client
}
func (chs *ClientHandlerService) GetClientSelectedBootOption(client string) (*bootoption.EFIApp, error) {
clientDetails, ok := chs.clients[client]
if !ok {
return nil, ErrUnknownClient
}
if clientDetails.SelectedOption == "" {
return nil, ErrUnselectedBootOption
}
for _, o := range clientDetails.Options {
if o.Name == clientDetails.SelectedOption {
return &o, nil
}
}
return nil, ErrUnknownBootOption
}
func (chs *ClientHandlerService) SetClientBootOption(client, option string) error {
clientDetails, ok := chs.clients[client]
if !ok {
return ErrUnknownClient
}
for _, o := range clientDetails.Options {
if o.Name == option {
clientDetails.SelectedOption = option
return nil
}
}
return ErrUnknownBootOption
}