You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
140 lines
4.2 KiB
140 lines
4.2 KiB
package managementaccess
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"time"
|
|
|
|
"tera-clone/services/tera-api/internal/domain"
|
|
)
|
|
|
|
const maxMessageBytes = 16 * 1024
|
|
|
|
type socketRequest struct {
|
|
Action string `json:"action"`
|
|
Settings domain.ManagementAccessSettings `json:"settings"`
|
|
}
|
|
|
|
type socketResponse struct {
|
|
OK bool `json:"ok"`
|
|
Code string `json:"code,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type SocketServer struct{ Controller Controller }
|
|
|
|
func (s SocketServer) Serve(listener net.Listener) error {
|
|
if s.Controller == nil {
|
|
return errors.New("management access controller is required")
|
|
}
|
|
for {
|
|
connection, err := listener.Accept()
|
|
if err != nil {
|
|
if errors.Is(err, net.ErrClosed) {
|
|
return nil
|
|
}
|
|
if temporary, ok := err.(interface{ Temporary() bool }); ok && temporary.Temporary() {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
go s.handle(connection)
|
|
}
|
|
}
|
|
|
|
func (s SocketServer) handle(connection net.Conn) {
|
|
defer connection.Close()
|
|
_ = connection.SetDeadline(time.Now().Add(12 * time.Second))
|
|
payload, err := bufio.NewReader(io.LimitReader(connection, maxMessageBytes+1)).ReadBytes('\n')
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
s.respond(connection, socketResponse{Code: "validation_failed", Error: "invalid management access request"})
|
|
return
|
|
}
|
|
if len(payload) == 0 || len(payload) > maxMessageBytes || payload[len(payload)-1] != '\n' {
|
|
s.respond(connection, socketResponse{Code: "validation_failed", Error: "invalid management access request"})
|
|
return
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(payload))
|
|
decoder.DisallowUnknownFields()
|
|
var request socketRequest
|
|
if err := decoder.Decode(&request); err != nil {
|
|
s.respond(connection, socketResponse{Code: "validation_failed", Error: "invalid management access request"})
|
|
return
|
|
}
|
|
var extra any
|
|
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
|
|
s.respond(connection, socketResponse{Code: "validation_failed", Error: "management access request must contain one JSON value"})
|
|
return
|
|
}
|
|
if request.Action != "apply" {
|
|
s.respond(connection, socketResponse{Code: "validation_failed", Error: "management access action is not supported"})
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := s.Controller.Apply(ctx, request.Settings); err != nil {
|
|
code := "management_access_failed"
|
|
if IsUnavailable(err) {
|
|
code = "management_access_unavailable"
|
|
}
|
|
s.respond(connection, socketResponse{Code: code, Error: err.Error()})
|
|
return
|
|
}
|
|
s.respond(connection, socketResponse{OK: true})
|
|
}
|
|
|
|
func (SocketServer) respond(connection net.Conn, response socketResponse) {
|
|
_ = json.NewEncoder(connection).Encode(response)
|
|
}
|
|
|
|
type SocketClient struct {
|
|
path string
|
|
timeout time.Duration
|
|
}
|
|
|
|
func NewSocketClient(path string) *SocketClient {
|
|
if path == "" {
|
|
path = DefaultSocketPath
|
|
}
|
|
return &SocketClient{path: path, timeout: 12 * time.Second}
|
|
}
|
|
|
|
func (c *SocketClient) Apply(ctx context.Context, settings domain.ManagementAccessSettings) error {
|
|
if err := Validate(settings); err != nil {
|
|
return err
|
|
}
|
|
dialer := net.Dialer{Timeout: c.timeout}
|
|
connection, err := dialer.DialContext(ctx, "unix", c.path)
|
|
if err != nil {
|
|
return &UnavailableError{Reason: "tera-accessd is not reachable"}
|
|
}
|
|
defer connection.Close()
|
|
deadline := time.Now().Add(c.timeout)
|
|
if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) {
|
|
deadline = contextDeadline
|
|
}
|
|
_ = connection.SetDeadline(deadline)
|
|
if err := json.NewEncoder(connection).Encode(socketRequest{Action: "apply", Settings: settings}); err != nil {
|
|
return errors.New("management access request failed")
|
|
}
|
|
var response socketResponse
|
|
if err := json.NewDecoder(io.LimitReader(connection, maxMessageBytes)).Decode(&response); err != nil {
|
|
return &UnavailableError{Reason: "tera-accessd returned no response"}
|
|
}
|
|
if response.Error != "" {
|
|
if response.Code == "management_access_unavailable" {
|
|
return &UnavailableError{Reason: response.Error}
|
|
}
|
|
return fmt.Errorf("tera-accessd: %s", response.Error)
|
|
}
|
|
if !response.OK {
|
|
return errors.New("tera-accessd rejected the request")
|
|
}
|
|
return nil
|
|
}
|
|
|