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.
 
 
 
 
 
 

169 lines
5.7 KiB

// Package updatesocket exposes tera-updated only over a group-restricted Unix socket.
package updatesocket
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
"time"
"tera-clone/services/tera-api/internal/update"
)
const DefaultSocketPath = "/run/tera-updated/control.sock"
type response struct {
Status *update.Status `json:"status,omitempty"`
Code string `json:"code,omitempty"`
Error string `json:"error,omitempty"`
}
type Server struct {
Controller update.Controller
MaxBytes int64
}
func (s Server) Serve(listener net.Listener) error {
if s.Controller == nil || s.MaxBytes <= 0 {
return errors.New("update controller and positive package limit are required")
}
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/status", s.handleStatus)
mux.HandleFunc("POST /v1/upload", s.handleUpload)
mux.HandleFunc("POST /v1/apply", s.handleApply)
mux.HandleFunc("POST /v1/factory-reset", s.handleFactoryReset)
server := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 30 * time.Second}
err := server.Serve(listener)
if errors.Is(err, http.ErrServerClosed) || errors.Is(err, net.ErrClosed) {
return nil
}
return err
}
func (s Server) handleStatus(w http.ResponseWriter, request *http.Request) {
status, err := s.Controller.Status(request.Context())
s.write(w, status, err)
}
func (s Server) handleUpload(w http.ResponseWriter, request *http.Request) {
if request.ContentLength <= 0 || request.ContentLength > s.MaxBytes {
s.write(w, update.Status{}, &update.Error{Code: "update_size_invalid", Message: "package content length is missing or exceeds the configured maximum"})
return
}
request.Body = http.MaxBytesReader(w, request.Body, s.MaxBytes)
status, err := s.Controller.Upload(request.Context(), update.UploadRequest{
Filename: request.Header.Get("X-Tera-Filename"), ContentLength: request.ContentLength,
Actor: request.Header.Get("X-Tera-Actor"), Body: request.Body,
})
s.write(w, status, err)
}
func (s Server) handleApply(w http.ResponseWriter, request *http.Request) {
status, err := s.Controller.Apply(request.Context(), update.ApplyRequest{Actor: request.Header.Get("X-Tera-Actor"), OperationID: request.Header.Get("X-Tera-Operation-ID")})
s.write(w, status, err)
}
func (Server) write(w http.ResponseWriter, status update.Status, err error) {
w.Header().Set("Content-Type", "application/json")
value := response{Status: &status}
if err != nil {
value.Code, value.Error = update.Code(err), err.Error()
if value.Code == "" {
value.Code = "update_failed"
}
}
_ = json.NewEncoder(w).Encode(value)
}
type Client struct {
uploadClient *http.Client
controlClient *http.Client
}
func NewClient(socketPath string) *Client {
if socketPath == "" {
socketPath = DefaultSocketPath
}
transport := &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
dialer := net.Dialer{Timeout: 5 * time.Second}
return dialer.DialContext(ctx, "unix", socketPath)
},
DisableKeepAlives: false,
}
return &Client{
uploadClient: &http.Client{Transport: transport},
controlClient: &http.Client{Transport: transport, Timeout: 10 * time.Second},
}
}
func (c *Client) Status(ctx context.Context) (update.Status, error) {
request, _ := http.NewRequestWithContext(ctx, http.MethodGet, "http://tera-updated/v1/status", nil)
return c.do(c.controlClient, request)
}
func (c *Client) Upload(ctx context.Context, value update.UploadRequest) (update.Status, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://tera-updated/v1/upload", io.NopCloser(value.Body))
if err != nil {
return update.Status{}, err
}
request.ContentLength = value.ContentLength
request.Header.Set("Content-Type", "application/octet-stream")
request.Header.Set("X-Tera-Filename", value.Filename)
request.Header.Set("X-Tera-Actor", value.Actor)
request.Header.Set("X-Tera-Content-Length", strconv.FormatInt(value.ContentLength, 10))
return c.do(c.uploadClient, request)
}
func (c *Client) Apply(ctx context.Context, value update.ApplyRequest) (update.Status, error) {
request, _ := http.NewRequestWithContext(ctx, http.MethodPost, "http://tera-updated/v1/apply", nil)
request.Header.Set("X-Tera-Actor", value.Actor)
request.Header.Set("X-Tera-Operation-ID", value.OperationID)
return c.do(c.controlClient, request)
}
func (c *Client) do(client *http.Client, request *http.Request) (update.Status, error) {
result, err := client.Do(request)
if err != nil {
return update.Status{}, &update.Error{Code: "update_unavailable", Message: "tera-updated is not reachable"}
}
defer result.Body.Close()
var value response
if err := json.NewDecoder(io.LimitReader(result.Body, 256*1024)).Decode(&value); err != nil {
return update.Status{}, &update.Error{Code: "update_unavailable", Message: "tera-updated returned an invalid response"}
}
if value.Error != "" {
return statusValue(value.Status), &update.Error{Code: value.Code, Message: value.Error}
}
if value.Status == nil {
return update.Status{}, fmt.Errorf("tera-updated returned no status")
}
return *value.Status, nil
}
func statusValue(value *update.Status) update.Status {
if value == nil {
return update.Status{}
}
return *value
}
func (s Server) handleFactoryReset(w http.ResponseWriter, request *http.Request) {
resetter, ok := s.Controller.(update.FactoryResetter)
if !ok {
s.write(w, update.Status{}, errors.New("firmware cleanup unavailable"))
return
}
err := resetter.FactoryReset(request.Context())
s.write(w, update.Status{State: update.StateIdle}, err)
}
func (c *Client) FactoryReset(ctx context.Context) error {
request, _ := http.NewRequestWithContext(ctx, http.MethodPost, "http://tera-updated/v1/factory-reset", nil)
_, err := c.do(c.controlClient, request)
return err
}