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