mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 07:16:38 +00:00
UI and CLI Clients are now able to use SSO login by default we will check if the management has configured or supports SSO providers daemon will handle fetching and waiting for an access token Oauth package was moved to internal to avoid one extra package at this stage Secrets were removed from OAuth CLI clients have less and better output 2 new status were introduced, NeedsLogin and FailedLogin for better messaging With NeedsLogin we no longer have endless login attempts
510 lines
12 KiB
Go
510 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"github.com/cenkalti/backoff/v4"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
_ "embed"
|
|
|
|
"github.com/getlantern/systray"
|
|
"github.com/netbirdio/netbird/client/internal"
|
|
"github.com/netbirdio/netbird/client/proto"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/skratchdot/open-golang/open"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/app"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
const (
|
|
defaultFailTimeout = 3 * time.Second
|
|
failFastTimeout = time.Second
|
|
)
|
|
|
|
func main() {
|
|
var daemonAddr string
|
|
|
|
defaultDaemonAddr := "unix:///var/run/wiretrustee.sock"
|
|
if runtime.GOOS == "windows" {
|
|
defaultDaemonAddr = "tcp://127.0.0.1:41731"
|
|
}
|
|
|
|
flag.StringVar(
|
|
&daemonAddr, "daemon-addr",
|
|
defaultDaemonAddr,
|
|
"Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
|
|
|
|
var showSettings bool
|
|
flag.BoolVar(&showSettings, "settings", false, "run settings windows")
|
|
|
|
flag.Parse()
|
|
|
|
a := app.New()
|
|
client := newServiceClient(daemonAddr, a, showSettings)
|
|
if showSettings {
|
|
a.Run()
|
|
} else {
|
|
if err := checkPIDFile(); err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
systray.Run(client.onTrayReady, client.onTrayExit)
|
|
}
|
|
}
|
|
|
|
//go:embed connected.ico
|
|
var iconConnectedICO []byte
|
|
|
|
//go:embed connected.png
|
|
var iconConnectedPNG []byte
|
|
|
|
//go:embed disconnected.ico
|
|
var iconDisconnectedICO []byte
|
|
|
|
//go:embed disconnected.png
|
|
var iconDisconnectedPNG []byte
|
|
|
|
type serviceClient struct {
|
|
ctx context.Context
|
|
addr string
|
|
conn proto.DaemonServiceClient
|
|
|
|
icConnected []byte
|
|
icDisconnected []byte
|
|
|
|
// systray menu itmes
|
|
mStatus *systray.MenuItem
|
|
mUp *systray.MenuItem
|
|
mDown *systray.MenuItem
|
|
mAdminPanel *systray.MenuItem
|
|
mSettings *systray.MenuItem
|
|
mQuit *systray.MenuItem
|
|
|
|
// application with main windows.
|
|
app fyne.App
|
|
wSettings fyne.Window
|
|
showSettings bool
|
|
|
|
// input elements for settings form
|
|
iMngURL *widget.Entry
|
|
iAdminURL *widget.Entry
|
|
iConfigFile *widget.Entry
|
|
iLogFile *widget.Entry
|
|
iPreSharedKey *widget.Entry
|
|
|
|
// observable settings over correspondign iMngURL and iPreSharedKey values.
|
|
managementURL string
|
|
preSharedKey string
|
|
adminURL string
|
|
}
|
|
|
|
// newServiceClient instance constructor
|
|
//
|
|
// This constructor olso build UI elements for settings window.
|
|
func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient {
|
|
s := &serviceClient{
|
|
ctx: context.Background(),
|
|
addr: addr,
|
|
app: a,
|
|
|
|
showSettings: showSettings,
|
|
}
|
|
|
|
if runtime.GOOS == "windows" {
|
|
s.icConnected = iconConnectedICO
|
|
s.icDisconnected = iconDisconnectedICO
|
|
} else {
|
|
s.icConnected = iconConnectedPNG
|
|
s.icDisconnected = iconDisconnectedPNG
|
|
}
|
|
|
|
if showSettings {
|
|
s.showUIElements()
|
|
return s
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *serviceClient) showUIElements() {
|
|
// add settings window UI elements.
|
|
s.wSettings = s.app.NewWindow("Settings")
|
|
s.iMngURL = widget.NewEntry()
|
|
s.iAdminURL = widget.NewEntry()
|
|
s.iConfigFile = widget.NewEntry()
|
|
s.iConfigFile.Disable()
|
|
s.iLogFile = widget.NewEntry()
|
|
s.iLogFile.Disable()
|
|
s.iPreSharedKey = widget.NewPasswordEntry()
|
|
s.wSettings.SetContent(s.getSettingsForm())
|
|
s.wSettings.Resize(fyne.NewSize(600, 100))
|
|
|
|
s.getSrvConfig()
|
|
|
|
s.wSettings.Show()
|
|
}
|
|
|
|
// getSettingsForm to embed it into settings window.
|
|
func (s *serviceClient) getSettingsForm() *widget.Form {
|
|
return &widget.Form{
|
|
Items: []*widget.FormItem{
|
|
{Text: "Management URL", Widget: s.iMngURL},
|
|
{Text: "Admin URL", Widget: s.iAdminURL},
|
|
{Text: "Pre-shared Key", Widget: s.iPreSharedKey},
|
|
{Text: "Config File", Widget: s.iConfigFile},
|
|
{Text: "Log File", Widget: s.iLogFile},
|
|
},
|
|
SubmitText: "Save",
|
|
OnSubmit: func() {
|
|
if s.iPreSharedKey.Text != "" && s.iPreSharedKey.Text != "**********" {
|
|
// validate preSharedKey if it added
|
|
if _, err := wgtypes.ParseKey(s.iPreSharedKey.Text); err != nil {
|
|
dialog.ShowError(fmt.Errorf("Invalid Pre-shared Key Value"), s.wSettings)
|
|
return
|
|
}
|
|
}
|
|
|
|
defer s.wSettings.Close()
|
|
// if management URL or Pre-shared key changed, we try to re-login with new settings.
|
|
if s.managementURL != s.iMngURL.Text || s.preSharedKey != s.iPreSharedKey.Text ||
|
|
s.adminURL != s.iAdminURL.Text {
|
|
|
|
s.managementURL = s.iMngURL.Text
|
|
s.preSharedKey = s.iPreSharedKey.Text
|
|
s.adminURL = s.iAdminURL.Text
|
|
|
|
client, err := s.getSrvClient(failFastTimeout)
|
|
if err != nil {
|
|
log.Errorf("get daemon client: %v", err)
|
|
return
|
|
}
|
|
|
|
_, err = client.Login(s.ctx, &proto.LoginRequest{
|
|
ManagementUrl: s.iMngURL.Text,
|
|
AdminURL: s.iAdminURL.Text,
|
|
PreSharedKey: s.iPreSharedKey.Text,
|
|
})
|
|
if err != nil {
|
|
log.Errorf("login to management URL: %v", err)
|
|
return
|
|
}
|
|
|
|
_, err = client.Up(s.ctx, &proto.UpRequest{})
|
|
if err != nil {
|
|
log.Errorf("login to management URL: %v", err)
|
|
return
|
|
}
|
|
|
|
}
|
|
s.wSettings.Close()
|
|
},
|
|
OnCancel: func() {
|
|
s.wSettings.Close()
|
|
},
|
|
}
|
|
}
|
|
|
|
func (s *serviceClient) login() error {
|
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
|
if err != nil {
|
|
log.Errorf("get client: %v", err)
|
|
return err
|
|
}
|
|
|
|
loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{})
|
|
if err != nil {
|
|
log.Errorf("login to management URL with: %v", err)
|
|
return err
|
|
}
|
|
|
|
if loginResp.NeedsSSOLogin {
|
|
err = open.Run(loginResp.VerificationURIComplete)
|
|
if err != nil {
|
|
log.Errorf("opening the verification uri in the browser failed: %v", err)
|
|
return err
|
|
}
|
|
|
|
_, err = conn.WaitSSOLogin(s.ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
|
|
if err != nil {
|
|
log.Errorf("waiting sso login failed with: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err := s.conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
|
log.Errorf("up service: %v", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *serviceClient) menuUpClick() error {
|
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
|
if err != nil {
|
|
log.Errorf("get client: %v", err)
|
|
return err
|
|
}
|
|
|
|
status, err := conn.Status(s.ctx, &proto.StatusRequest{})
|
|
if err != nil {
|
|
log.Errorf("get service status: %v", err)
|
|
return err
|
|
}
|
|
|
|
if status.Status == string(internal.StatusNeedsLogin) || status.Status == string(internal.StatusLoginFailed) {
|
|
err = s.login()
|
|
if err != nil {
|
|
log.Errorf("get service status: %v", err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
if status.Status != string(internal.StatusIdle) {
|
|
log.Warnf("already connected")
|
|
return nil
|
|
}
|
|
|
|
if _, err := s.conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
|
log.Errorf("up service: %v", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *serviceClient) menuDownClick() error {
|
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
|
if err != nil {
|
|
log.Errorf("get client: %v", err)
|
|
return err
|
|
}
|
|
|
|
status, err := conn.Status(s.ctx, &proto.StatusRequest{})
|
|
if err != nil {
|
|
log.Errorf("get service status: %v", err)
|
|
return err
|
|
}
|
|
|
|
if status.Status != string(internal.StatusConnected) {
|
|
log.Warnf("already down")
|
|
return nil
|
|
}
|
|
|
|
if _, err := s.conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
|
|
log.Errorf("down service: %v", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *serviceClient) updateStatus() error {
|
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = backoff.Retry(func() error {
|
|
status, err := conn.Status(s.ctx, &proto.StatusRequest{})
|
|
if err != nil {
|
|
log.Errorf("get service status: %v", err)
|
|
return err
|
|
}
|
|
|
|
if status.Status == string(internal.StatusConnected) {
|
|
systray.SetIcon(s.icConnected)
|
|
s.mStatus.SetTitle("Connected")
|
|
s.mUp.Disable()
|
|
s.mDown.Enable()
|
|
} else {
|
|
systray.SetIcon(s.icDisconnected)
|
|
s.mStatus.SetTitle("Disconnected")
|
|
s.mDown.Disable()
|
|
s.mUp.Enable()
|
|
}
|
|
return nil
|
|
}, &backoff.ExponentialBackOff{
|
|
InitialInterval: time.Second,
|
|
RandomizationFactor: backoff.DefaultRandomizationFactor,
|
|
Multiplier: backoff.DefaultMultiplier,
|
|
MaxInterval: 300 * time.Millisecond,
|
|
MaxElapsedTime: 2 * time.Second,
|
|
Stop: backoff.Stop,
|
|
Clock: backoff.SystemClock,
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *serviceClient) onTrayReady() {
|
|
systray.SetIcon(s.icDisconnected)
|
|
|
|
// setup systray menu items
|
|
s.mStatus = systray.AddMenuItem("Disconnected", "Disconnected")
|
|
s.mStatus.Disable()
|
|
systray.AddSeparator()
|
|
s.mUp = systray.AddMenuItem("Connect", "Connect")
|
|
s.mDown = systray.AddMenuItem("Disconnect", "Disconnect")
|
|
s.mDown.Disable()
|
|
s.mAdminPanel = systray.AddMenuItem("Admin Panel", "Wiretrustee Admin Panel")
|
|
systray.AddSeparator()
|
|
s.mSettings = systray.AddMenuItem("Settings", "Settings of the application")
|
|
systray.AddSeparator()
|
|
s.mQuit = systray.AddMenuItem("Quit", "Quit the client app")
|
|
|
|
go func() {
|
|
s.getSrvConfig()
|
|
for {
|
|
err := s.updateStatus()
|
|
if err != nil {
|
|
log.Errorf("error while updating status: %v", err)
|
|
}
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
var err error
|
|
for {
|
|
select {
|
|
case <-s.mAdminPanel.ClickedCh:
|
|
err = open.Run(s.adminURL)
|
|
case <-s.mUp.ClickedCh:
|
|
s.mUp.Disable()
|
|
if err = s.menuUpClick(); err != nil {
|
|
s.mUp.Enable()
|
|
}
|
|
case <-s.mDown.ClickedCh:
|
|
s.mDown.Disable()
|
|
if err = s.menuDownClick(); err != nil {
|
|
s.mDown.Enable()
|
|
}
|
|
case <-s.mSettings.ClickedCh:
|
|
s.mSettings.Disable()
|
|
go func() {
|
|
defer s.mSettings.Enable()
|
|
proc, err := os.Executable()
|
|
if err != nil {
|
|
log.Errorf("show settings: %v", err)
|
|
return
|
|
}
|
|
|
|
cmd := exec.Command(proc, "--settings=true")
|
|
out, err := cmd.CombinedOutput()
|
|
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
|
|
log.Errorf("start settings UI: %v, %s", err, string(out))
|
|
return
|
|
}
|
|
if len(out) != 0 {
|
|
log.Info("settings change:", string(out))
|
|
}
|
|
|
|
// update config in systray when settings windows closed
|
|
s.getSrvConfig()
|
|
}()
|
|
case <-s.mQuit.ClickedCh:
|
|
systray.Quit()
|
|
return
|
|
}
|
|
if err != nil {
|
|
log.Errorf("process connection: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (s *serviceClient) onTrayExit() {}
|
|
|
|
// getSrvClient connection to the service.
|
|
func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonServiceClient, error) {
|
|
if s.conn != nil {
|
|
return s.conn, nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
conn, err := grpc.DialContext(
|
|
ctx,
|
|
strings.TrimPrefix(s.addr, "tcp://"),
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
grpc.WithBlock(),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dial service: %w", err)
|
|
}
|
|
|
|
s.conn = proto.NewDaemonServiceClient(conn)
|
|
return s.conn, nil
|
|
}
|
|
|
|
// getSrvConfig from the service to show it in the settings window.
|
|
func (s *serviceClient) getSrvConfig() {
|
|
s.managementURL = "https://api.wiretrustee.com:33073"
|
|
s.adminURL = "https://app.netbird.io"
|
|
|
|
conn, err := s.getSrvClient(failFastTimeout)
|
|
if err != nil {
|
|
log.Errorf("get client: %v", err)
|
|
return
|
|
}
|
|
|
|
cfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{})
|
|
if err != nil {
|
|
log.Errorf("get config settings from server: %v", err)
|
|
return
|
|
}
|
|
|
|
if cfg.ManagementUrl != "" {
|
|
s.managementURL = cfg.ManagementUrl
|
|
}
|
|
if cfg.AdminURL != "" {
|
|
s.adminURL = cfg.AdminURL
|
|
}
|
|
s.preSharedKey = cfg.PreSharedKey
|
|
|
|
if s.showSettings {
|
|
s.iMngURL.SetText(s.managementURL)
|
|
s.iAdminURL.SetText(s.adminURL)
|
|
s.iConfigFile.SetText(cfg.ConfigFile)
|
|
s.iLogFile.SetText(cfg.LogFile)
|
|
s.iPreSharedKey.SetText(cfg.PreSharedKey)
|
|
}
|
|
}
|
|
|
|
// checkPIDFile exists and return error, or write new.
|
|
func checkPIDFile() error {
|
|
pidFile := path.Join(os.TempDir(), "wiretrustee-ui.pid")
|
|
if piddata, err := ioutil.ReadFile(pidFile); err == nil {
|
|
if pid, err := strconv.Atoi(string(piddata)); err == nil {
|
|
if process, err := os.FindProcess(pid); err == nil {
|
|
if err := process.Signal(syscall.Signal(0)); err == nil {
|
|
return fmt.Errorf("process already exists: %d", pid)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return ioutil.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0o664)
|
|
}
|