Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

macos presence detection #1867

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9d05605
first pass poc at macos presence detection
James-Pickett Sep 13, 2024
97f4287
naming
James-Pickett Sep 13, 2024
566d5bd
rough exec implementation of presence detection
James-Pickett Sep 17, 2024
ecd9d3e
add headers to v2CmdRequestType, act on headers in presence detection…
James-Pickett Sep 18, 2024
d7efbab
dont show desktop until requested by runner
James-Pickett Sep 19, 2024
4b99368
do presence detection through desktop server
James-Pickett Sep 20, 2024
c1eb82d
clean up, lint
James-Pickett Sep 20, 2024
635ec1b
look for reason in header with default, remove unneeded struct
James-Pickett Sep 23, 2024
c87a128
put c in own file
James-Pickett Sep 23, 2024
47fbb32
dont use desktop runner singleton
James-Pickett Sep 23, 2024
1fd46c9
build tags in c files
James-Pickett Sep 23, 2024
888643f
presence detection tests
James-Pickett Sep 23, 2024
ddc3ada
test that headers are present after opening krypto challenge
James-Pickett Sep 23, 2024
9524d0d
update log
James-Pickett Sep 24, 2024
cd65542
dont display desktop in test, just manage process
James-Pickett Sep 24, 2024
6f3bb1a
increase runner test start up time
James-Pickett Sep 24, 2024
15a981d
presence detector return durtion since last detection, add to local s…
James-Pickett Sep 26, 2024
aa824b1
tweaks
James-Pickett Sep 26, 2024
8747dd0
skip presence test on non darwin
James-Pickett Sep 26, 2024
e12a332
simplify presence detector
James-Pickett Sep 26, 2024
dcaa01e
presence detection via ec middleware
James-Pickett Sep 27, 2024
98c8c51
feedback
James-Pickett Sep 27, 2024
4d952b2
make presence detection part of test darwin only
James-Pickett Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions cmd/launcher/desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ func runDesktop(_ *multislogger.MultiSlogger, args []string) error {
}, func(error) {})

shutdownChan := make(chan struct{})
server, err := userserver.New(slogger, *flUserServerAuthToken, *flUserServerSocketPath, shutdownChan, notifier)
showDesktopChan := make(chan struct{})

server, err := userserver.New(slogger, *flUserServerAuthToken, *flUserServerSocketPath, shutdownChan, showDesktopChan, notifier)
if err != nil {
return err
}
Expand Down Expand Up @@ -182,9 +184,10 @@ func runDesktop(_ *multislogger.MultiSlogger, args []string) error {
}
}()

// block until a send on showDesktopChan
<-showDesktopChan
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh, this is tidy!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was torn between this route and just passing a flag when we spin up desktop. We are able to show the desktop on demand, but as far as I can tell, there is no way to hide the desktop on demand. When you tell systray to shutdown, it exits the program completely. So if we ever want to hide it, we have to kill the process and let a new one spin up hidden.

I chose this route because I think it's rare that we would turn off the desktop and I don't want to be starting / killing processes on a first install where a user is trying auth for the first time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's rare that we would turn off the desktop

I agree, I think that's a reasonable assumption -- this route makes sense to me

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think users will, but I think your reasoning is sound.

// blocks until shutdown called
m.Init()

return nil
}

Expand Down
48 changes: 48 additions & 0 deletions cmd/launcher/detect_presence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package main

import (
"encoding/base64"
"encoding/json"
"errors"
"os"
"runtime"

"github.com/kolide/launcher/ee/presencedetection"
"github.com/kolide/launcher/pkg/log/multislogger"
)

func runDetectPresence(_ *multislogger.MultiSlogger, args []string) error {
reason := ""

if len(args) != 0 {
reason = args[0]
}

if reason == "" && runtime.GOOS == "darwin" {
return errors.New("reason is required on darwin")
}

success, err := presencedetection.Detect(reason)
response := presencedetection.PresenceDetectionResponse{
Success: success,
}
if err != nil {
response.Error = err.Error()
}

// serialize response to JSON
responseJSON, err := json.Marshal(response)
if err != nil {
return err
}

// b64 enode response
responseB64 := base64.StdEncoding.EncodeToString(responseJSON)

// write response to stdout
if _, err := os.Stdout.Write([]byte(responseB64)); err != nil {
return err
}

return nil
}
2 changes: 2 additions & 0 deletions cmd/launcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ func runSubcommands(systemMultiSlogger *multislogger.MultiSlogger) error {
run = runSecureEnclave
case "watchdog": // note: this is currently only implemented for windows
run = watchdog.RunWatchdogService
case "detect-presence":
run = runDetectPresence
default:
return fmt.Errorf("unknown subcommand %s", os.Args[1])
}
Expand Down
81 changes: 54 additions & 27 deletions ee/desktop/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,6 @@ type DesktopUsersProcessesRunner struct {
// usersFilesRoot is the launcher root dir with will be the parent dir
// for kolide desktop files on a per user basis
usersFilesRoot string
// processSpawningEnabled controls whether or not desktop user processes are automatically spawned
// This effectively represents whether or not the launcher desktop GUI is enabled or not
processSpawningEnabled bool
// knapsack is the almighty sack of knaps
knapsack types.Knapsack
// runnerServer is a local server that desktop processes call to monitor parent
Expand Down Expand Up @@ -155,17 +152,16 @@ func (pr processRecord) String() string {
// New creates and returns a new DesktopUsersProcessesRunner runner and initializes all required fields
func New(k types.Knapsack, messenger runnerserver.Messenger, opts ...desktopUsersProcessesRunnerOption) (*DesktopUsersProcessesRunner, error) {
runner := &DesktopUsersProcessesRunner{
interrupt: make(chan struct{}),
uidProcs: make(map[string]processRecord),
updateInterval: k.DesktopUpdateInterval(),
menuRefreshInterval: k.DesktopMenuRefreshInterval(),
procsWg: &sync.WaitGroup{},
interruptTimeout: time.Second * 5,
hostname: k.KolideServerURL(),
usersFilesRoot: agent.TempPath("kolide-desktop"),
processSpawningEnabled: k.DesktopEnabled(),
knapsack: k,
cachedMenuData: newMenuItemCache(),
interrupt: make(chan struct{}),
uidProcs: make(map[string]processRecord),
updateInterval: k.DesktopUpdateInterval(),
menuRefreshInterval: k.DesktopMenuRefreshInterval(),
procsWg: &sync.WaitGroup{},
interruptTimeout: time.Second * 5,
hostname: k.KolideServerURL(),
usersFilesRoot: agent.TempPath("kolide-desktop"),
knapsack: k,
cachedMenuData: newMenuItemCache(),
}

runner.slogger = k.Slogger().With("component", "desktop_runner")
Expand Down Expand Up @@ -452,12 +448,35 @@ func (r *DesktopUsersProcessesRunner) Update(data io.Reader) error {
}

func (r *DesktopUsersProcessesRunner) FlagsChanged(flagKeys ...keys.FlagKey) {
if slices.Contains(flagKeys, keys.DesktopEnabled) {
r.processSpawningEnabled = r.knapsack.DesktopEnabled()
r.slogger.Log(context.TODO(), slog.LevelDebug,
"runner processSpawningEnabled set by control server",
"process_spawning_enabled", r.processSpawningEnabled,
)
if !slices.Contains(flagKeys, keys.DesktopEnabled) {
return
}

r.slogger.Log(context.TODO(), slog.LevelDebug,
"desktop enabled set by control server",
"process_spawning_enabled", r.knapsack.DesktopEnabled(),
zackattack01 marked this conversation as resolved.
Show resolved Hide resolved
)

if !r.knapsack.DesktopEnabled() {
// there is no way to "hide" the menu, so we will just kill any existing processes
// they will respawn in "silent" mode
r.killDesktopProcesses(context.TODO())
return
}

// DesktopEnabled() == true
// Tell any running desktop user processes that they should show the menu
for uid, proc := range r.uidProcs {
client := client.New(r.userServerAuthToken, proc.socketPath)
if err := client.ShowDesktop(); err != nil {
r.slogger.Log(context.TODO(), slog.LevelError,
"sending refresh command to user desktop process",
"uid", uid,
"pid", proc.Process.Pid,
"path", proc.path,
"err", err,
)
}
}
}

Expand All @@ -483,6 +502,10 @@ func (r *DesktopUsersProcessesRunner) writeSharedFile(path string, data []byte)

// refreshMenu updates the menu file and tells desktop processes to refresh their menus
func (r *DesktopUsersProcessesRunner) refreshMenu() {
if !r.knapsack.DesktopEnabled() {
return
}

if err := r.generateMenuFile(); err != nil {
if r.knapsack.DebugServerData() {
r.slogger.Log(context.TODO(), slog.LevelError,
Expand All @@ -502,8 +525,18 @@ func (r *DesktopUsersProcessesRunner) refreshMenu() {
// Tell any running desktop user processes that they should refresh the latest menu data
for uid, proc := range r.uidProcs {
client := client.New(r.userServerAuthToken, proc.socketPath)
if err := client.Refresh(); err != nil {

if err := client.ShowDesktop(); err != nil {
r.slogger.Log(context.TODO(), slog.LevelError,
"sending refresh command to user desktop process",
"uid", uid,
"pid", proc.Process.Pid,
"path", proc.path,
"err", err,
)
}

if err := client.Refresh(); err != nil {
r.slogger.Log(context.TODO(), slog.LevelError,
"sending refresh command to user desktop process",
"uid", uid,
Expand Down Expand Up @@ -601,12 +634,6 @@ func (r *DesktopUsersProcessesRunner) runConsoleUserDesktop() error {
return nil
}

if !r.processSpawningEnabled {
// Desktop is disabled, kill any existing desktop user processes
r.killDesktopProcesses(context.Background())
return nil
}

executablePath, err := r.determineExecutablePath()
if err != nil {
return fmt.Errorf("determining executable path: %w", err)
Expand Down
4 changes: 4 additions & 0 deletions ee/desktop/user/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ func (c *client) Refresh() error {
return c.get("refresh")
}

func (c *client) ShowDesktop() error {
return c.get("show")
}

func (c *client) Notify(n notify.Notification) error {
notificationToSend := notify.Notification{
Title: n.Title,
Expand Down
2 changes: 1 addition & 1 deletion ee/desktop/user/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestClient_GetAndShutdown(t *testing.T) {

socketPath := testSocketPath(t)
shutdownChan := make(chan struct{})
server, err := server.New(multislogger.NewNopLogger(), validAuthToken, socketPath, shutdownChan, nil)
server, err := server.New(multislogger.NewNopLogger(), validAuthToken, socketPath, shutdownChan, make(chan<- struct{}), nil)
require.NoError(t, err)

go func() {
Expand Down
30 changes: 24 additions & 6 deletions ee/desktop/user/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"os"
"runtime"
"strings"
"sync"
"time"

"github.com/kolide/launcher/ee/desktop/user/notify"
Expand All @@ -30,26 +31,34 @@ type UserServer struct {
server *http.Server
listener net.Listener
shutdownChan chan<- struct{}
showDesktopChan chan<- struct{}
authToken string
socketPath string
notifier notificationSender
refreshListeners []func()
}

func New(slogger *slog.Logger, authToken string, socketPath string, shutdownChan chan<- struct{}, notifier notificationSender) (*UserServer, error) {
func New(slogger *slog.Logger,
authToken string,
socketPath string,
shutdownChan chan<- struct{},
showDesktopChan chan<- struct{},
notifier notificationSender) (*UserServer, error) {
userServer := &UserServer{
shutdownChan: shutdownChan,
authToken: authToken,
slogger: slogger.With("component", "desktop_server"),
socketPath: socketPath,
notifier: notifier,
shutdownChan: shutdownChan,
showDesktopChan: showDesktopChan,
authToken: authToken,
slogger: slogger.With("component", "desktop_server"),
socketPath: socketPath,
notifier: notifier,
}

authedMux := http.NewServeMux()
authedMux.HandleFunc("/shutdown", userServer.shutdownHandler)
authedMux.HandleFunc("/ping", userServer.pingHandler)
authedMux.HandleFunc("/notification", userServer.notificationHandler)
authedMux.HandleFunc("/refresh", userServer.refreshHandler)
authedMux.HandleFunc("/show", userServer.showDesktop)

userServer.server = &http.Server{
Handler: userServer.authMiddleware(authedMux),
Expand Down Expand Up @@ -152,6 +161,15 @@ func (s *UserServer) notificationHandler(w http.ResponseWriter, req *http.Reques
w.WriteHeader(http.StatusOK)
}

func (s *UserServer) showDesktop(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

sync.OnceFunc(func() {
s.showDesktopChan <- struct{}{}
})()
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
}

func (s *UserServer) refreshHandler(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
Expand Down
2 changes: 1 addition & 1 deletion ee/desktop/user/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func testServer(t *testing.T, authHeader, socketPath string, logBytes *bytes.Buf
Level: slog.LevelDebug,
}))

server, err := New(slogger, authHeader, socketPath, shutdownChan, nil)
server, err := New(slogger, authHeader, socketPath, shutdownChan, make(chan<- struct{}), nil)
require.NoError(t, err)
return server, shutdownChan
}
Expand Down
16 changes: 12 additions & 4 deletions ee/localserver/krypto-ec-middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ import (
)

const (
timestampValidityRange = 150
kolideKryptoEccHeader20230130Value = "2023-01-30"
kolideKryptoHeaderKey = "X-Kolide-Krypto"
kolideSessionIdHeaderKey = "X-Kolide-Session"
timestampValidityRange = 150
kolideKryptoEccHeader20230130Value = "2023-01-30"
kolideKryptoHeaderKey = "X-Kolide-Krypto"
kolideSessionIdHeaderKey = "X-Kolide-Session"
kolidePresenceDetectionIntervalSecondsKey = "X-Kolide-Presence-Detection-Interval"
)

type v2CmdRequestType struct {
Path string
Body []byte
Headers map[string][]string
CallbackUrl string
CallbackHeaders map[string][]string
AllowedOrigins []string
Expand Down Expand Up @@ -285,6 +287,12 @@ func (e *kryptoEcMiddleware) Wrap(next http.Handler) http.Handler {
},
}

for h, vals := range cmdReq.Headers {
for _, v := range vals {
newReq.Header.Add(h, v)
}
}

newReq.Header.Set("Origin", r.Header.Get("Origin"))
newReq.Header.Set("Referer", r.Header.Get("Referer"))

Expand Down
Loading
Loading