feat(node): package Ubuntu desktop setup and trusted access
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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("/") }}
|
||||
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()
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user