bastion 0.53.2
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
// tray_pipe_windows.go — `privatae-bastion tray`: the signed-in person's icon
|
||||
// beside a bastion that runs as the machine's service.
|
||||
//
|
||||
// The service runs in session 0 and has no desktop (tray_windows.go), so the
|
||||
// icon is a separate process: this verb, started at every sign-in by the
|
||||
// machine-wide logon entry `service install` writes (tray.go trayRunValue),
|
||||
// running as whoever signed in, unelevated. It holds nothing of the
|
||||
// service's: it asks over the approvals pipe (approvalpipe_windows.go) what is
|
||||
// waiting, shows it, and sends the person's code for one request. The secret
|
||||
// the code is checked against stays in the service.
|
||||
//
|
||||
// ONE PER SESSION. The logon entry and `npx cseries` may both start it; a
|
||||
// session-local mutex makes the second one exit at once.
|
||||
//
|
||||
// THE CONSOLE. The bastion is one console program for the command line, the
|
||||
// service and this, so Windows gives a process started from the logon entry a
|
||||
// console window of its own. The tray lets go of it at once when it is the
|
||||
// only process on it (started that way, not from a terminal); the window may
|
||||
// show for a moment at sign-in.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"fyne.io/systray"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
modKernel32Tray = windows.NewLazySystemDLL("kernel32.dll")
|
||||
procGetConsoleProcessList = modKernel32Tray.NewProc("GetConsoleProcessList")
|
||||
procFreeConsole = modKernel32Tray.NewProc("FreeConsole")
|
||||
)
|
||||
|
||||
// letGoOfOwnConsole frees a console that only this process is on.
|
||||
func letGoOfOwnConsole() {
|
||||
var pids [2]uint32
|
||||
n, _, _ := procGetConsoleProcessList.Call(uintptr(unsafe.Pointer(&pids[0])), 2)
|
||||
if n == 1 {
|
||||
procFreeConsole.Call()
|
||||
}
|
||||
}
|
||||
|
||||
func cmdTray() error {
|
||||
if trayFor(runtime.GOOS, runningAsService, systemService.installed, "tray") != trayPipe {
|
||||
return errors.New("this machine's bastion is not a system service, so it has no separate tray: " +
|
||||
"its icon shows while `privatae-bastion run` runs")
|
||||
}
|
||||
name, err := windows.UTF16PtrFromString(`Local\PrivataeBastionTray`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m, err := windows.CreateMutex(nil, false, name)
|
||||
if errors.Is(err, windows.ERROR_ALREADY_EXISTS) {
|
||||
if m != 0 {
|
||||
_ = windows.CloseHandle(m)
|
||||
}
|
||||
return nil // a tray already runs in this session
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot start the tray: %w", err)
|
||||
}
|
||||
defer windows.CloseHandle(m)
|
||||
letGoOfOwnConsole()
|
||||
|
||||
page, err := startTrayPage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer page.close()
|
||||
approvalsOpener = page.open
|
||||
// systray's own init locked the main goroutine to the main thread, and
|
||||
// main runs this verb on it, so Run is called right here (tray_windows.go
|
||||
// explains why that matters).
|
||||
systray.Run(func() { trayPipeReady(page) }, func() {})
|
||||
return nil
|
||||
}
|
||||
|
||||
// trayPage is the tray's approvals window (uitray.go), served on loopback by
|
||||
// this process.
|
||||
type trayPage struct {
|
||||
srv *uiServer
|
||||
addr string
|
||||
h *http.Server
|
||||
}
|
||||
|
||||
func startTrayPage() (*trayPage, error) {
|
||||
// LOOPBACK ONLY, any free port: the page draws what a Host asks of this
|
||||
// machine, and nothing off it has any business reading that.
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the tray cannot open its approvals window: %w", err)
|
||||
}
|
||||
_, port, _ := net.SplitHostPort(ln.Addr().String())
|
||||
secret, err := genSecret(32)
|
||||
if err != nil {
|
||||
_ = ln.Close()
|
||||
return nil, err
|
||||
}
|
||||
srv, err := newUIServer(port, secret)
|
||||
if err != nil {
|
||||
_ = ln.Close()
|
||||
return nil, err
|
||||
}
|
||||
srv.trayPipe = true
|
||||
srv.pipe = pipeCall
|
||||
h := &http.Server{Handler: srv.handler(), ReadHeaderTimeout: 5 * time.Second}
|
||||
go func() { _ = h.Serve(ln) }()
|
||||
return &trayPage{srv: srv, addr: ln.Addr().String(), h: h}, nil
|
||||
}
|
||||
|
||||
// open shows the approvals window, signed in by a ticket minted in-process.
|
||||
func (p *trayPage) open(from string) {
|
||||
tok, err := p.srv.tickets.mint()
|
||||
if err != nil {
|
||||
uilogf("%s: %v\n", from, err)
|
||||
return
|
||||
}
|
||||
if err := openPopupWindow("http://" + p.addr + "/?k=" + tok); err != nil {
|
||||
uilogf("%s: %v\n", from, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *trayPage) close() {
|
||||
sh, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
_ = p.h.Shutdown(sh)
|
||||
}
|
||||
|
||||
func trayPipeReady(page *trayPage) {
|
||||
systray.SetIcon(trayIcon("bad"))
|
||||
systray.SetTooltip("Privatae bastion")
|
||||
mState := systray.AddMenuItem("Checking…", "")
|
||||
mState.Disable()
|
||||
mApprovals := systray.AddMenuItem(trayApprovalsLine(0), "Open what your hosts are waiting on you for")
|
||||
mApprovals.Disable()
|
||||
systray.AddSeparator()
|
||||
// NOT "Stop the bastion": the bastion is the machine's service, and it is
|
||||
// whoever administers the machine who stops it. This closes the icon only.
|
||||
mQuit := systray.AddMenuItem("Close this icon",
|
||||
"The bastion keeps running as this machine's service; the icon comes back at your next sign-in")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
notes := &approvalNotifier{}
|
||||
sink := startApprovalNotifier(ctx)
|
||||
var last string
|
||||
refresh := func() {
|
||||
s := trayPipeStatus(pipeCall)
|
||||
if s.StateClass != last {
|
||||
if b := trayIcon(s.StateClass); b != nil {
|
||||
systray.SetIcon(b)
|
||||
}
|
||||
last = s.StateClass
|
||||
}
|
||||
line := trayStateLineFrom(s)
|
||||
systray.SetTooltip(clipUTF16("Privatae bastion — "+line, 127))
|
||||
mState.SetTitle(line)
|
||||
pending := approvalsForNotice(s)
|
||||
mApprovals.SetTitle(trayApprovalsLine(len(pending)))
|
||||
if len(pending) > 0 {
|
||||
mApprovals.Enable()
|
||||
} else {
|
||||
mApprovals.Disable()
|
||||
}
|
||||
notes.poll(time.Now(), pending, sink)
|
||||
}
|
||||
refresh()
|
||||
go func() {
|
||||
t := time.NewTicker(trayRefresh)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
refresh()
|
||||
case <-mApprovals.ClickedCh:
|
||||
go page.open("tray")
|
||||
case <-mQuit.ClickedCh:
|
||||
cancel()
|
||||
systray.Quit()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user