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.

247 lines
8.5 KiB

// Package timesocket exposes the narrow group-restricted tera-timed protocol.
package timesocket
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"time"
"tera-clone/services/tera-api/internal/systemtime"
)
const DefaultSocketPath = "/run/tera-timed/time.sock"
type request struct {
Action string `json:"action"`
BrowserTime time.Time `json:"browser_time,omitempty"`
Timezone string `json:"timezone,omitempty"`
Servers []string `json:"servers,omitempty"`
Settings *systemtime.Settings `json:"settings,omitempty"`
}
type response struct {
Result *systemtime.Result `json:"result,omitempty"`
NTPResult *systemtime.NTPResult `json:"ntp_result,omitempty"`
Code string `json:"code,omitempty"`
Error string `json:"error,omitempty"`
}
type Server struct {
Controller systemtime.Controller
}
func (s Server) Serve(listener net.Listener) error {
if s.Controller == nil {
return errors.New("system time 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.handleConnection(connection)
}
}
func (s Server) handleConnection(connection net.Conn) {
defer connection.Close()
_ = connection.SetDeadline(time.Now().Add(10 * time.Second))
payload, err := bufio.NewReader(io.LimitReader(connection, 4097)).ReadBytes('\n')
if err != nil && !errors.Is(err, io.EOF) {
s.writeResponse(connection, response{Code: "validation_failed", Error: "invalid system time request"})
return
}
if len(payload) == 0 || len(payload) > 4096 || payload[len(payload)-1] != '\n' {
s.writeResponse(connection, response{Code: "validation_failed", Error: "invalid system time request"})
return
}
decoder := json.NewDecoder(bytes.NewReader(payload))
decoder.DisallowUnknownFields()
var command request
if err := decoder.Decode(&command); err != nil {
s.writeResponse(connection, response{Code: "validation_failed", Error: "invalid system time request"})
return
}
var extra any
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
s.writeResponse(connection, response{Code: "validation_failed", Error: "system time request must contain a single JSON value"})
return
}
if command.Action != "sync" && command.Action != "sync_ntp" && command.Action != "configure" {
s.writeResponse(connection, response{Code: "validation_failed", Error: "system time action is not supported"})
return
}
timeout := 8 * time.Second
if command.Action == "sync_ntp" {
timeout = 25 * time.Second
}
_ = connection.SetDeadline(time.Now().Add(timeout + 2*time.Second))
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if command.Action == "configure" {
controller, ok := s.Controller.(systemtime.SettingsController)
if !ok || command.Settings == nil {
s.writeResponse(connection, response{Error: "time settings controller unavailable"})
return
}
if err := controller.Configure(ctx, *command.Settings); err != nil {
s.writeResponse(connection, response{Error: err.Error()})
return
}
s.writeResponse(connection, response{Result: &systemtime.Result{Timezone: command.Settings.Timezone}})
return
}
if command.Action == "sync_ntp" {
controller, ok := s.Controller.(systemtime.NTPController)
if !ok {
s.writeResponse(connection, response{Code: "time_unavailable", Error: "tera-timed has no NTP controller"})
return
}
result, err := controller.SyncNTP(ctx, systemtime.NTPRequest{Servers: command.Servers, Timezone: command.Timezone})
if err != nil {
code := "time_failed"
if systemtime.IsUnavailable(err) {
code = "time_unavailable"
} else if _, validationErr := systemtime.NormalizeNTPRequest(systemtime.NTPRequest{Servers: command.Servers, Timezone: command.Timezone}); validationErr != nil {
code = "validation_failed"
}
s.writeResponse(connection, response{Code: code, Error: err.Error()})
return
}
s.writeResponse(connection, response{NTPResult: &result})
return
}
result, err := s.Controller.Sync(ctx, systemtime.Request{BrowserTime: command.BrowserTime, Timezone: command.Timezone})
if err != nil {
code := "time_failed"
if systemtime.IsUnavailable(err) {
code = "time_unavailable"
} else if _, validationErr := systemtime.NormalizeRequest(systemtime.Request{BrowserTime: command.BrowserTime, Timezone: command.Timezone}); validationErr != nil {
code = "validation_failed"
}
s.writeResponse(connection, response{Code: code, Error: err.Error()})
return
}
s.writeResponse(connection, response{Result: &result})
}
func (Server) writeResponse(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: 30 * time.Second}
}
func (c *Client) Sync(ctx context.Context, value systemtime.Request) (systemtime.Result, error) {
command := request{Action: "sync", BrowserTime: value.BrowserTime, Timezone: value.Timezone}
dialer := net.Dialer{Timeout: c.timeout}
connection, err := dialer.DialContext(ctx, "unix", c.path)
if err != nil {
return systemtime.Result{}, &systemtime.UnavailableError{Reason: "tera-timed 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 systemtime.Result{}, errors.New("system time request failed")
}
var result response
if err := json.NewDecoder(io.LimitReader(connection, 65536)).Decode(&result); err != nil {
return systemtime.Result{}, &systemtime.UnavailableError{Reason: "tera-timed returned no response"}
}
if result.Error != "" {
if result.Code == "time_unavailable" {
return systemtime.Result{}, &systemtime.UnavailableError{Reason: result.Error}
}
return systemtime.Result{}, errors.New(result.Error)
}
if result.Result == nil {
return systemtime.Result{}, fmt.Errorf("tera-timed returned no sync result")
}
return *result.Result, nil
}
func (c *Client) SyncNTP(ctx context.Context, value systemtime.NTPRequest) (systemtime.NTPResult, error) {
command := request{Action: "sync_ntp", Servers: value.Servers, Timezone: value.Timezone}
dialer := net.Dialer{Timeout: c.timeout}
connection, err := dialer.DialContext(ctx, "unix", c.path)
if err != nil {
return systemtime.NTPResult{}, &systemtime.UnavailableError{Reason: "tera-timed 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 systemtime.NTPResult{}, errors.New("NTP synchronization request failed")
}
var result response
if err := json.NewDecoder(io.LimitReader(connection, 65536)).Decode(&result); err != nil {
return systemtime.NTPResult{}, &systemtime.UnavailableError{Reason: "tera-timed returned no NTP response"}
}
if result.Error != "" {
if result.Code == "time_unavailable" {
return systemtime.NTPResult{}, &systemtime.UnavailableError{Reason: result.Error}
}
return systemtime.NTPResult{}, errors.New(result.Error)
}
if result.NTPResult == nil {
return systemtime.NTPResult{}, errors.New("tera-timed returned no NTP sync result")
}
return *result.NTPResult, nil
}
func (c *Client) Configure(ctx context.Context, settings systemtime.Settings) error {
connection, err := (&net.Dialer{Timeout: c.timeout}).DialContext(ctx, "unix", c.path)
if err != nil {
return err
}
defer connection.Close()
deadline := time.Now().Add(c.timeout)
if d, ok := ctx.Deadline(); ok && d.Before(deadline) {
deadline = d
}
_ = connection.SetDeadline(deadline)
if err := json.NewEncoder(connection).Encode(request{Action: "configure", Settings: &settings}); err != nil {
return err
}
var result response
if err := json.NewDecoder(io.LimitReader(connection, 65536)).Decode(&result); err != nil {
return err
}
if result.Error != "" {
return errors.New(result.Error)
}
if result.Result == nil {
return errors.New("time settings acknowledgement missing")
}
return nil
}