// 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 "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 }