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.

178 lines
5.4 KiB

// Package alarmsocket exposes the narrow group-restricted tera-alarmd protocol.
package alarmsocket
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net"
"time"
"tera-clone/services/tera-api/internal/alarmdelivery"
)
const DefaultSocketPath = "/run/tera-alarmd/alarm-delivery.sock"
type command struct {
Action string `json:"action"`
DeliveryID string `json:"delivery_id,omitempty"`
Request alarmdelivery.Request `json:"request,omitempty"`
}
type response struct {
Result *alarmdelivery.Result `json:"result,omitempty"`
Code string `json:"code,omitempty"`
Error string `json:"error,omitempty"`
}
type Server struct{ Controller alarmdelivery.Controller }
func (s Server) Serve(listener net.Listener) error {
if s.Controller == nil {
return errors.New("alarm delivery 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 Server) handle(connection net.Conn) {
defer connection.Close()
_ = connection.SetDeadline(time.Now().Add(12 * time.Second))
payload, err := bufio.NewReader(io.LimitReader(connection, 32769)).ReadBytes('\n')
if err != nil && !errors.Is(err, io.EOF) {
s.write(connection, response{Code: "validation_failed", Error: "invalid alarm delivery request"})
return
}
if len(payload) == 0 || len(payload) > 32768 || payload[len(payload)-1] != '\n' {
s.write(connection, response{Code: "validation_failed", Error: "invalid alarm delivery request"})
return
}
decoder := json.NewDecoder(bytes.NewReader(payload))
decoder.DisallowUnknownFields()
var value command
if err := decoder.Decode(&value); err != nil {
s.write(connection, response{Code: "validation_failed", Error: "invalid alarm delivery request"})
return
}
var extra any
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
s.write(connection, response{Code: "validation_failed", Error: "alarm delivery request must contain one JSON value"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var result alarmdelivery.Result
switch value.Action {
case "health":
result.State = "ready"
case "factory_reset":
resetter, ok := s.Controller.(alarmdelivery.FactoryResetter)
if !ok {
err = errors.New("alarm factory reset unavailable")
} else {
err = resetter.FactoryReset(ctx)
}
case "deliver":
result, err = s.Controller.Deliver(ctx, value.Request)
case "status":
result, err = s.Controller.Status(ctx, value.DeliveryID)
default:
s.write(connection, response{Code: "validation_failed", Error: "alarm delivery action is not supported"})
return
}
if err != nil {
code := "delivery_failed"
if alarmdelivery.IsUnavailable(err) {
code = "delivery_unavailable"
}
s.write(connection, response{Code: code, Error: err.Error()})
return
}
s.write(connection, response{Result: &result})
}
func (Server) write(connection net.Conn, value response) {
_ = json.NewEncoder(connection).Encode(value)
}
type Client struct {
path string
timeout time.Duration
}
func NewClient(path string) *Client {
if path == "" {
path = DefaultSocketPath
}
return &Client{path: path, timeout: 12 * time.Second}
}
func (c *Client) Deliver(ctx context.Context, request alarmdelivery.Request) (alarmdelivery.Result, error) {
return c.call(ctx, command{Action: "deliver", Request: request})
}
func (c *Client) Status(ctx context.Context, deliveryID string) (alarmdelivery.Result, error) {
return c.call(ctx, command{Action: "status", DeliveryID: deliveryID})
}
func (c *Client) call(ctx context.Context, command command) (alarmdelivery.Result, error) {
dialer := net.Dialer{Timeout: c.timeout}
connection, err := dialer.DialContext(ctx, "unix", c.path)
if err != nil {
return alarmdelivery.Result{}, &alarmdelivery.UnavailableError{Reason: "tera-alarmd 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(command); err != nil {
return alarmdelivery.Result{}, &alarmdelivery.UnavailableError{Reason: "alarm delivery request failed"}
}
var value response
if err := json.NewDecoder(io.LimitReader(connection, 65536)).Decode(&value); err != nil {
return alarmdelivery.Result{}, &alarmdelivery.UnavailableError{Reason: "tera-alarmd returned no response"}
}
if value.Error != "" {
if value.Code == "delivery_unavailable" {
return alarmdelivery.Result{}, &alarmdelivery.UnavailableError{Reason: value.Error}
}
return alarmdelivery.Result{}, errors.New(value.Error)
}
if value.Result == nil {
return alarmdelivery.Result{}, errors.New("tera-alarmd returned no delivery result")
}
return *value.Result, nil
}
func (c *Client) FactoryReset(ctx context.Context) error {
_, err := c.call(ctx, command{Action: "factory_reset"})
return err
}
func (c *Client) Health(ctx context.Context) error {
result, err := c.call(ctx, command{Action: "health"})
if err != nil {
return err
}
if result.State != "ready" {
return errors.New("alarm service not ready")
}
return nil
}