Skip to content

Commit

Permalink
support setting system proxy when vpn switch is toggled
Browse files Browse the repository at this point in the history
  • Loading branch information
atavism committed Nov 9, 2023
1 parent 495f66c commit bc0ba20
Show file tree
Hide file tree
Showing 18 changed files with 1,287 additions and 0 deletions.
145 changes: 145 additions & 0 deletions desktop/app/sysproxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package app

import (
"fmt"
"path/filepath"
"strings"
"sync"
"time"

"github.com/getlantern/errors"
"github.com/getlantern/filepersist"
"github.com/getlantern/flashlight/v7/client"
"github.com/getlantern/flashlight/v7/common"
"github.com/getlantern/flashlight/v7/ops"
"github.com/getlantern/sysproxy"

"github.com/getlantern/android-lantern/desktop/icons"
)

var (
_sysproxyOff func() error
sysproxyOffMx sync.Mutex
)

func setUpSysproxyTool() error {
var iconFile string
if common.Platform == "darwin" {
icon, err := icons.Asset(appIcon("connected"))
if err != nil {
return fmt.Errorf("unable to load escalation prompt icon: %v", err)
}
// We have to use a short filepath here because Cocoa won't display the
// icon if the path is too long.
iconFile = filepath.Join("/tmp", appIcon("escalate"))
err = filepersist.Save(iconFile, icon, 0644)
if err != nil {
log.Errorf("Unable to persist icon to disk, fallback to default icon: %v", err)
} else {
log.Debugf("Saved icon file to: %v", iconFile)
}
}
err := sysproxy.EnsureHelperToolPresent("sysproxy-cmd", "Lantern would like to be your system proxy", iconFile)
if err != nil {
return fmt.Errorf("unable to set up sysproxy setting tool: %v", err)
}
return nil
}

func SysproxyOn() (err error) {
op := ops.Begin("sysproxy_on")
defer op.End()
addr, found := getProxyAddr()
if !found {
err = errors.New("Unable to set lantern as system proxy, no proxy address available")
op.FailIf(log.Error(err))
return
}
log.Debugf("Setting lantern as system proxy at: %v", addr)
off, e := sysproxy.On(addr)
if e != nil {
err = errors.New("Unable to set lantern as system proxy: %v", e)
op.FailIf(log.Error(err))
return
}
sysproxyOffMx.Lock()
_sysproxyOff = off
sysproxyOffMx.Unlock()
log.Debug("Finished setting lantern as system proxy")
return
}

func SysProxyOff() {
sysproxyOffMx.Lock()
off := _sysproxyOff
_sysproxyOff = nil
sysproxyOffMx.Unlock()

if off != nil {
doSysproxyOff(off)
}

op := ops.Begin("sysproxy_off_force")
defer op.End()
log.Debug("Force clearing system proxy directly, just in case")
addr, found := getProxyAddr()
if !found {
op.FailIf(log.Error("Unable to find proxy address, can't force clear system proxy"))
return
}
doSysproxyClear(op, addr)
}

func doSysproxyOff(off func() error) {
op := ops.Begin("sysproxy_off")
defer op.End()
log.Debug("Unsetting lantern as system proxy using off function")
err := off()
if err != nil {
op.FailIf(log.Errorf("Unable to unset lantern as system proxy using off function: %v", err))
return
}
log.Debug("Unset lantern as system proxy using off function")
}

// clearSysproxyFor is like sysproxyOffFor, but records its activity under the
// sysproxy_clear op instead of the sysproxy_off op.
func clearSysproxyFor(addr string) {
op := ops.Begin("sysproxy_clear")
doSysproxyClear(op, addr)
op.End()
}

func doSysproxyClear(op *ops.Op, addr string) {
log.Debugf("Clearing lantern as system proxy at: %v", addr)
err := sysproxy.Off(addr)
if err != nil {
op.FailIf(log.Errorf("Unable to clear lantern as system proxy: %v", err))
} else {
log.Debug("Cleared lantern as system proxy")
}
}

func getProxyAddr() (addr string, found bool) {
var _addr interface{}
_addr, found = client.Addr(5 * time.Minute)
if found {
addr = _addr.(string)
}
return
}

func appIcon(name string) string {
return strings.ToLower(common.DefaultAppName) + "_" + fmt.Sprintf(iconTemplate(), name)
}

func iconTemplate() string {
if common.Platform == "darwin" {
if common.DefaultAppName == "Beam" {
return "%s_32.png"
}
// Lantern doesn't have png files to support dark mode yet
return "%s_32.ico"
}
return "%s_32.ico"
}
189 changes: 189 additions & 0 deletions desktop/app/systray.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
//go:build !headless
// +build !headless

package app

import (
"fmt"
"sync"

"github.com/getlantern/flashlight/v7/common"
"github.com/getlantern/flashlight/v7/stats"
"github.com/getlantern/i18n"
"github.com/getlantern/systray"

"github.com/getlantern/lantern-desktop/icons"
)

var menu struct {
enable bool
st stats.Stats
stMx sync.RWMutex
status *systray.MenuItem
toggle *systray.MenuItem
show *systray.MenuItem
upgrade *systray.MenuItem
quit *systray.MenuItem
}

var iconsByName = make(map[string][]byte)

type systrayCallbacks interface {
WaitForExit() error
AddExitFunc(string, func())
Exit(error) bool
OnTrayShow()
OnTrayUpgrade()
Connect()
Disconnect()
OnStatsChange(func(stats.Stats))
}

func RunOnSystrayReady(standalone bool, a systrayCallbacks, onReady func()) {
onExit := func() {
if a.Exit(nil) {
err := a.WaitForExit()
if err != nil {
log.Errorf("Error exiting app: %v", err)
}
}
}

if standalone {
log.Error("Standalone mode currently not supported, opening in system browser")
// TODO: re-enable standalone mode when systray library has been stabilized
// systray.RunWithAppWindow(i18n.T("TRAY_LANTERN"), 1024, 768, onReady, onExit)
// } else {
}
systray.Run(onReady, onExit)
}

func QuitSystray(a *App) {
// Typically, systray.Quit will actually be what causes the app to exit, but
// in the case of an uncaught Fatal error or CTRL-C, the app will exit before the
// systray and we need it to call systray.Quit().
systray.Quit()
}

func configureSystemTray(a systrayCallbacks) error {
for _, name := range []string{"connected", "connectedalert", "disconnected", "disconnectedalert"} {
icon, err := icons.Asset(appIcon(name))
if err != nil {
return fmt.Errorf("Unable to load %v icon for system tray: %v", name, err)
}
iconsByName[name] = icon
}

menu.status = systray.AddMenuItem("", "")
menu.status.Disable()
menu.toggle = systray.AddMenuItem("", "")
systray.AddSeparator()
menu.upgrade = systray.AddMenuItem("", "")
menu.show = systray.AddMenuItem("", "")
systray.AddSeparator()
menu.quit = systray.AddMenuItem("", "")
refreshMenuItems()

// Suppress showing "Update to Pro" until user status is got from pro-server.
if common.ProAvailable {
menu.st.IsPro = true
} else {
menu.upgrade.Hide()
}

menu.st.Status = stats.STATUS_CONNECTING
statsUpdated()
a.OnStatsChange(func(newStats stats.Stats) {
menu.stMx.Lock()
menu.st = newStats
menu.stMx.Unlock()
statsUpdated()
})

go func() {
for {
select {
case <-menu.toggle.ClickedCh:
menu.stMx.Lock()
disconnected := menu.st.Disconnected
menu.stMx.Unlock()
if disconnected {
a.Connect()
} else {
a.Disconnect()
}
case <-menu.show.ClickedCh:
a.OnTrayShow()
case <-menu.upgrade.ClickedCh:
a.OnTrayUpgrade()
case <-menu.quit.ClickedCh:
systray.Quit()
}
}
}()

return nil
}

func refreshSystray(language string) {
if !menu.enable {
return
}
if _, err := i18n.SetLocale(language); err != nil {
log.Errorf("i18n.SetLocale(%s) failed: %q", language, err)
return
}
refreshMenuItems()
statsUpdated()
}

func refreshMenuItems() {
menu.upgrade.SetTitle(i18n.T("TRAY_UPGRADE_TO_PRO"))
systray.SetTooltip(i18n.T(translationAppName))
menu.show.SetTitle(i18n.T("TRAY_SHOW", i18n.T(translationAppName)))
menu.quit.SetTitle(i18n.T("TRAY_QUIT", i18n.T(translationAppName)))
}

func statsUpdated() {
menu.stMx.RLock()
st := menu.st
menu.stMx.RUnlock()

iconName := "connected"
statusKey := st.Status
if st.Disconnected || !st.HasSucceedingProxy {
iconName = "disconnected"
}

if st.HitDataCap && !st.IsPro {
iconName += "alert"
if !st.Disconnected {
statusKey = "throttled"
}
} else if len(st.Alerts) > 0 {
iconName += "alert"
// Show the first one as status if there are multiple alerts
statusKey = st.Alerts[0].Alert()
}

if common.DefaultAppName == "Beam" {
systray.SetTemplateIcon(iconsByName[iconName], iconsByName[iconName])
} else {
// Lantern icons do not support dark mode yet
systray.SetIcon(iconsByName[iconName])
}
status := i18n.T("TRAY_STATUS", i18n.T("status."+statusKey))
menu.status.SetTitle(status)

if st.IsPro || !common.ProAvailable {
menu.upgrade.Hide()
} else {
menu.upgrade.Show()
}

if st.Disconnected {
menu.toggle.SetTitle(i18n.T("TRAY_CONNECT"))
} else {
menu.toggle.SetTitle(i18n.T("TRAY_DISCONNECT"))
}
}
Loading

0 comments on commit bc0ba20

Please sign in to comment.