Files
NODEDC_MISSION_CORE/apps/node-agent/cmd/node-agent/main.go
T

164 lines
4.8 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/fs"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"nodedc.local/mission-core/node-agent/internal/node"
"nodedc.local/mission-core/node-agent/web"
)
var version = "0.2.0"
const defaultSocket = "/run/mission-core-node/admin.sock"
func main() {
if err := run(); err != nil {
log.Print(err)
os.Exit(1)
}
}
func run() error {
if len(os.Args) > 1 && os.Args[1] == "authorize" {
return authorize(defaultSocket)
}
if len(os.Args) == 3 && os.Args[1] == "ssh-keys" {
value, err := node.AuthorizedKeys("/var/lib/mission-core-node/ssh-keys.json", os.Args[2])
if err == nil {
fmt.Print(value)
}
return err
}
flags := flag.NewFlagSet("node-agent", flag.ContinueOnError)
dir := flags.String("state", "/var/lib/mission-core-node", "private state directory")
socket := flags.String("socket", defaultSocket, "private launcher socket")
listen := flags.String("listen", "127.0.0.1:8780", "loopback UI address")
if err := flags.Parse(os.Args[1:]); err != nil {
return err
}
host, _, err := net.SplitHostPort(*listen)
if err != nil || host != "127.0.0.1" {
return errors.New("local UI must bind 127.0.0.1")
}
// Bind before touching state/socket; a second instance cannot replace identity or launcher authority.
tcp, err := net.Listen("tcp", *listen)
if err != nil {
return err
}
defer tcp.Close()
store, err := node.OpenStore(*dir)
if err != nil {
return err
}
assets, err := fs.Sub(web.Assets, "dist")
if err != nil {
return err
}
app := &node.Server{Store: store, Assets: assets, Origin: "http://" + *listen, Version: version, Inventory: func() node.Inventory { return node.Host("/") }}
pairing, err := node.OpenPairing(store, *dir, version, app.Inventory)
if err != nil {
return err
}
app.Pairing = pairing
nodeID, _ := store.Public()
app.Sensors, err = node.OpenSensors(*dir, nodeID)
if err != nil {
return err
}
pairing.Sensors = app.Sensors
app.DeviceEnrollment, err = node.OpenDeviceEnrollment(*dir, nodeID)
if err != nil {
return err
}
pairing.DeviceEnrollment = app.DeviceEnrollment
app.Sensors.NetworkDevices = app.DeviceEnrollment
app.Access = &node.AccessStore{Path: filepath.Join(*dir, "ssh-keys.json"), Users: func() []string { return node.LocalAdmins("/") }}
if err := os.MkdirAll(filepath.Dir(*socket), 0700); err != nil {
return err
}
if info, e := os.Lstat(*socket); e == nil {
if info.Mode()&os.ModeSocket == 0 {
return errors.New("launcher path is not a socket")
}
if err := os.Remove(*socket); err != nil {
return err
}
} else if !os.IsNotExist(e) {
return e
}
unix, err := net.Listen("unix", *socket)
if err != nil {
return err
}
defer unix.Close()
defer os.Remove(*socket)
if err := os.Chmod(*socket, 0600); err != nil {
return err
}
admin := http.NewServeMux()
admin.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"url": app.IssueLogin()})
})
public := &http.Server{Handler: app.Handler(), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 30 * time.Second, MaxHeaderBytes: 8192}
private := &http.Server{Handler: admin, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 10 * time.Second, MaxHeaderBytes: 8192}
errs := make(chan error, 2)
go func() { errs <- public.Serve(tcp) }()
go func() { errs <- private.Serve(unix) }()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go app.Sensors.WatchUSB(ctx)
go pairing.Run(ctx)
log.Print("Mission Core Node " + version + " listening on loopback")
select {
case err = <-errs:
case <-ctx.Done():
}
shutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
public.Shutdown(shutdown)
private.Shutdown(shutdown)
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
}
func authorize(socket string) error {
// Called by a fixed, root-owned polkit helper. No user-supplied URL, command,
// path, or environment is interpreted by the privileged operation.
client := &http.Client{Timeout: 5 * time.Second, Transport: &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "unix", socket)
}}}
res, err := client.Post("http://local/login", "application/json", nil)
if err != nil {
return errors.New("Node is unavailable")
}
defer res.Body.Close()
if res.StatusCode != 200 {
return errors.New("Node rejected local authorization")
}
var value struct {
URL string `json:"url"`
}
if err := json.NewDecoder(io.LimitReader(res.Body, 1024)).Decode(&value); err != nil {
return err
}
fmt.Print(value.URL)
return nil
}