Add first routes and log middleware handling
This commit is contained in:
parent
b8bb280d0a
commit
9f39de9bca
9 changed files with 341 additions and 22 deletions
14
bootserver/bootoption/bootoption.go
Normal file
14
bootserver/bootoption/bootoption.go
Normal 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"`
|
||||||
|
}
|
85
bootserver/controllers/client/client.go
Normal file
85
bootserver/controllers/client/client.go
Normal 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)
|
||||||
|
}
|
61
bootserver/controllers/client/enroll.go
Normal file
61
bootserver/controllers/client/enroll.go
Normal 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)
|
||||||
|
}
|
31
bootserver/helpers/helpers.go
Normal file
31
bootserver/helpers/helpers.go
Normal 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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
39
bootserver/middlewares/logger.go
Normal file
39
bootserver/middlewares/logger.go
Normal 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)
|
||||||
|
}
|
23
bootserver/middlewares/middlewarechain.go
Normal file
23
bootserver/middlewares/middlewarechain.go
Normal 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}},
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,10 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,6 +24,8 @@ type Server struct {
|
||||||
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{
|
||||||
|
@ -70,11 +80,15 @@ func New(appConf *config.AppConfig, logger *logrus.Logger) (*Server, error) {
|
||||||
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!"))
|
|
||||||
}
|
|
||||||
|
|
58
bootserver/services/services.go
Normal file
58
bootserver/services/services.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue