// 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"` } 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" { 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 == "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 }