aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui/tunnelspage.go
diff options
context:
space:
mode:
authorAlexander Neumann <alexander.neumann@picos-software.com>2019-04-24 15:29:38 +0200
committerJason A. Donenfeld <Jason@zx2c4.com>2019-04-24 15:42:51 +0200
commit4a0ee91473619db8a2c8a100758f1c24c39e7e39 (patch)
tree5df163a3e81b948fdf05f26429739d12b41ffc2e /ui/tunnelspage.go
parentui: programmatically compute colors (diff)
downloadwireguard-windows-4a0ee91473619db8a2c8a100758f1c24c39e7e39.tar.xz
wireguard-windows-4a0ee91473619db8a2c8a100758f1c24c39e7e39.zip
ui: use tabs in main window and refactor tunnels ui and log dialog into tab pages
requires https://github.com/lxn/walk/commit/edb74ee350e9585ddd212acad445ec383950f2cc for status image background Signed-off-by: Alexander Neumann <alexander.neumann@picos-software.com>
Diffstat (limited to 'ui/tunnelspage.go')
-rw-r--r--ui/tunnelspage.go403
1 files changed, 403 insertions, 0 deletions
diff --git a/ui/tunnelspage.go b/ui/tunnelspage.go
new file mode 100644
index 00000000..ac778b3a
--- /dev/null
+++ b/ui/tunnelspage.go
@@ -0,0 +1,403 @@
+/* SPDX-License-Identifier: MIT
+ *
+ * Copyright (C) 2019 WireGuard LLC. All Rights Reserved.
+ */
+
+package ui
+
+import (
+ "archive/zip"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/lxn/walk"
+ "golang.zx2c4.com/wireguard/windows/conf"
+ "golang.zx2c4.com/wireguard/windows/service"
+)
+
+type TunnelsPage struct {
+ *walk.TabPage
+
+ tunnelTracker *TunnelTracker
+ tunnelsView *TunnelsView
+ confView *ConfView
+ tunnelAddedPublisher walk.StringEventPublisher
+ tunnelDeletedPublisher walk.StringEventPublisher
+}
+
+func NewTunnelsPage() (*TunnelsPage, error) {
+ var err error
+
+ var disposables walk.Disposables
+ defer disposables.Treat()
+
+ tp := new(TunnelsPage)
+ if tp.TabPage, err = walk.NewTabPage(); err != nil {
+ return nil, err
+ }
+ disposables.Add(tp)
+
+ tp.SetTitle("Tunnels")
+ tp.SetLayout(walk.NewHBoxLayout())
+
+ tunnelsContainer, _ := walk.NewComposite(tp)
+ tunnelsContainer.SetLayout(walk.NewVBoxLayout())
+
+ tp.tunnelsView, _ = NewTunnelsView(tunnelsContainer)
+ tp.tunnelsView.ItemActivated().Attach(tp.onTunnelsViewItemActivated)
+ tp.tunnelsView.CurrentIndexChanged().Attach(tp.updateConfView)
+
+ // ToolBar actions
+ {
+ // HACK: Because of https://github.com/lxn/walk/issues/481
+ // we need to put the ToolBar into its own Composite.
+ toolBarContainer, _ := walk.NewComposite(tunnelsContainer)
+ toolBarContainer.SetLayout(walk.NewHBoxLayout())
+
+ tunnelsToolBar, _ := walk.NewToolBarWithOrientationAndButtonStyle(toolBarContainer, walk.Horizontal, walk.ToolBarButtonTextOnly)
+
+ importAction := walk.NewAction()
+ importAction.SetText("Import tunnels from file...")
+ importAction.Triggered().Attach(tp.onImport)
+
+ addAction := walk.NewAction()
+ addAction.SetText("Add empty tunnel")
+ addAction.Triggered().Attach(tp.onAddTunnel)
+
+ exportTunnelsAction := walk.NewAction()
+ exportTunnelsAction.SetText("Export tunnels to zip...")
+ exportTunnelsAction.Triggered().Attach(tp.onExportTunnels)
+
+ addMenu, _ := walk.NewMenu()
+ tp.AddDisposable(addMenu)
+ addMenu.Actions().Add(addAction)
+ addMenu.Actions().Add(importAction)
+ addMenuAction, _ := tunnelsToolBar.Actions().AddMenu(addMenu)
+ addMenuAction.SetText("➕")
+
+ deleteAction := walk.NewAction()
+ tunnelsToolBar.Actions().Add(deleteAction)
+ deleteAction.SetText("➖")
+ deleteAction.Triggered().Attach(tp.onDelete)
+
+ settingsMenu, _ := walk.NewMenu()
+ tp.AddDisposable(settingsMenu)
+ settingsMenu.Actions().Add(exportTunnelsAction)
+ settingsMenuAction, _ := tunnelsToolBar.Actions().AddMenu(settingsMenu)
+ settingsMenuAction.SetText("⚙")
+ }
+
+ currentTunnelContainer, _ := walk.NewComposite(tp)
+ currentTunnelContainer.SetLayout(walk.NewVBoxLayout())
+ tp.Layout().(interface{ SetStretchFactor(walk.Widget, int) error }).SetStretchFactor(currentTunnelContainer, 10)
+
+ tp.confView, _ = NewConfView(currentTunnelContainer)
+
+ updateConfViewTicker := time.NewTicker(time.Second)
+ tp.Disposing().Attach(updateConfViewTicker.Stop)
+ go func() {
+ for range updateConfViewTicker.C {
+ tp.Synchronize(func() {
+ tp.updateConfView()
+ })
+ }
+ }()
+
+ controlsContainer, _ := walk.NewComposite(currentTunnelContainer)
+ controlsContainer.SetLayout(walk.NewHBoxLayout())
+ controlsContainer.Layout().SetMargins(walk.Margins{})
+
+ walk.NewHSpacer(controlsContainer)
+
+ editTunnel, _ := walk.NewPushButton(controlsContainer)
+ editTunnel.SetEnabled(false)
+ tp.tunnelsView.CurrentIndexChanged().Attach(func() {
+ editTunnel.SetEnabled(tp.tunnelsView.CurrentIndex() > -1)
+ })
+ editTunnel.SetText("Edit")
+ editTunnel.Clicked().Attach(tp.onEditTunnel)
+
+ tp.tunnelsView.SetCurrentIndex(0)
+
+ disposables.Spare()
+
+ return tp, nil
+}
+
+func (tp *TunnelsPage) TunnelTracker() *TunnelTracker {
+ return tp.tunnelTracker
+}
+
+func (tp *TunnelsPage) SetTunnelTracker(tunnelTracker *TunnelTracker) {
+ tp.tunnelTracker = tunnelTracker
+
+ tp.confView.SetTunnelTracker(tunnelTracker)
+}
+
+func (tp *TunnelsPage) SetTunnelState(tunnel *service.Tunnel, state service.TunnelState) {
+ tp.tunnelsView.SetTunnelState(tunnel, state)
+}
+
+func (tp *TunnelsPage) updateConfView() {
+ if !tp.Visible() {
+ return
+ }
+
+ tp.confView.SetTunnel(tp.tunnelsView.CurrentTunnel())
+}
+
+// importFiles tries to import a list of configurations.
+func (tp *TunnelsPage) importFiles(paths []string) {
+ type unparsedConfig struct {
+ Name string
+ Config string
+ }
+
+ var (
+ unparsedConfigs []unparsedConfig
+ lastErr error
+ )
+
+ // Note: other versions of WireGuard start with all .zip files, then all .conf files.
+ // To reproduce that if needed, inverse-sort the array.
+ for _, path := range paths {
+ switch filepath.Ext(path) {
+ case ".conf":
+ textConfig, err := ioutil.ReadFile(path)
+ if err != nil {
+ lastErr = err
+ continue
+ }
+ unparsedConfigs = append(unparsedConfigs, unparsedConfig{Name: strings.TrimSuffix(filepath.Base(path), ".conf"), Config: string(textConfig)})
+ case ".zip":
+ // 1 .conf + 1 error .zip edge case?
+ r, err := zip.OpenReader(path)
+ if err != nil {
+ lastErr = err
+ continue
+ }
+
+ for _, f := range r.File {
+ if filepath.Ext(f.Name) != ".conf" {
+ continue
+ }
+
+ rc, err := f.Open()
+ if err != nil {
+ lastErr = err
+ continue
+ }
+ textConfig, err := ioutil.ReadAll(rc)
+ rc.Close()
+ if err != nil {
+ lastErr = err
+ continue
+ }
+ unparsedConfigs = append(unparsedConfigs, unparsedConfig{Name: strings.TrimSuffix(filepath.Base(f.Name), ".conf"), Config: string(textConfig)})
+ }
+
+ r.Close()
+ }
+ }
+
+ if lastErr != nil || unparsedConfigs == nil {
+ walk.MsgBox(tp.Form(), "Error", fmt.Sprintf("Could not parse some files: %v", lastErr), walk.MsgBoxIconWarning)
+ return
+ }
+
+ var configs []*conf.Config
+
+ for _, unparsedConfig := range unparsedConfigs {
+ config, err := conf.FromWgQuick(unparsedConfig.Config, unparsedConfig.Name)
+ if err != nil {
+ lastErr = err
+ continue
+ }
+ service.IPCClientNewTunnel(config)
+ configs = append(configs, config)
+ }
+
+ m, n := len(configs), len(unparsedConfigs)
+ switch {
+ case n == 1 && m != n:
+ walk.MsgBox(tp.Form(), "Error", fmt.Sprintf("Could not parse some files: %v", lastErr), walk.MsgBoxIconWarning)
+ case n == 1 && m == n:
+ // TODO: Select tunnel in the list
+ case m == n:
+ walk.MsgBox(tp.Form(), "Imported tunnels", fmt.Sprintf("Imported %d tunnels", m), walk.MsgBoxOK)
+ case m != n:
+ walk.MsgBox(tp.Form(), "Imported tunnels", fmt.Sprintf("Imported %d of %d tunnels", m, n), walk.MsgBoxIconWarning)
+ default:
+ panic("unreachable case")
+ }
+}
+
+func (tp *TunnelsPage) exportTunnels(filePath string) {
+ writeFileWithOverwriteHandling(tp.Form(), filePath, func(file *os.File) error {
+ writer := zip.NewWriter(file)
+
+ for _, tunnel := range tp.tunnelsView.model.tunnels {
+ cfg, err := tunnel.StoredConfig()
+ if err != nil {
+ return fmt.Errorf("onExportTunnels: tunnel.StoredConfig failed: %v", err)
+ }
+
+ w, err := writer.Create(tunnel.Name + ".conf")
+ if err != nil {
+ return fmt.Errorf("onExportTunnels: writer.Create failed: %v", err)
+ }
+
+ if _, err := w.Write(([]byte)(cfg.ToWgQuick())); err != nil {
+ return fmt.Errorf("onExportTunnels: cfg.ToWgQuick failed: %v", err)
+ }
+ }
+
+ return writer.Close()
+ })
+}
+
+func (tp *TunnelsPage) addTunnel(config *conf.Config) {
+ tunnel, err := service.IPCClientNewTunnel(config)
+ if err != nil {
+ walk.MsgBox(tp.Form(), "Unable to create tunnel", err.Error(), walk.MsgBoxIconError)
+ return
+ }
+
+ model := tp.tunnelsView.model
+ model.tunnels = append(model.tunnels, tunnel)
+ model.PublishRowsReset()
+ model.Sort(model.SortedColumn(), model.SortOrder())
+
+ for i, t := range model.tunnels {
+ if t.Name == tunnel.Name {
+ tp.tunnelsView.SetCurrentIndex(i)
+ break
+ }
+ }
+
+ tp.confView.SetTunnel(&tunnel)
+
+ tp.tunnelAddedPublisher.Publish(tunnel.Name)
+}
+
+func (tp *TunnelsPage) deleteTunnel(tunnel *service.Tunnel) {
+ tunnel.Delete()
+
+ model := tp.tunnelsView.model
+
+ for i, t := range model.tunnels {
+ if t.Name == tunnel.Name {
+ model.tunnels = append(model.tunnels[:i], model.tunnels[i+1:]...)
+ model.PublishRowsRemoved(i, i)
+ break
+ }
+ }
+
+ tp.tunnelDeletedPublisher.Publish(tunnel.Name)
+}
+
+func (tp *TunnelsPage) TunnelAdded() *walk.StringEvent {
+ return tp.tunnelAddedPublisher.Event()
+}
+
+func (tp *TunnelsPage) TunnelDeleted() *walk.StringEvent {
+ return tp.tunnelDeletedPublisher.Event()
+}
+
+// Handlers
+
+func (tp *TunnelsPage) onTunnelsViewItemActivated() {
+ if tp.tunnelTracker.InTransition() {
+ return
+ }
+
+ var err error
+ var title string
+ tunnel := tp.tunnelsView.CurrentTunnel()
+ activeTunnel := tp.tunnelTracker.ActiveTunnel()
+ if tunnel != nil && activeTunnel != nil && tunnel.Name == activeTunnel.Name {
+ err, title = tp.tunnelTracker.DeactivateTunnel(), "Deactivating tunnel failed"
+ } else {
+ err, title = tp.tunnelTracker.ActivateTunnel(tunnel), "Activating tunnel failed"
+ }
+ if err != nil {
+ walk.MsgBox(tp.Form(), title, fmt.Sprintf("Error: %s", err.Error()), walk.MsgBoxIconError)
+ }
+}
+
+func (tp *TunnelsPage) onEditTunnel() {
+ tunnel := tp.tunnelsView.CurrentTunnel()
+ if tunnel == nil {
+ // Misfired event?
+ return
+ }
+
+ if config := runTunnelConfigDialog(tp.Form(), tunnel); config != nil {
+ // Delete old one
+ tp.deleteTunnel(tunnel)
+
+ // Save new one
+ tp.addTunnel(config)
+ }
+}
+
+func (tp *TunnelsPage) onAddTunnel() {
+ if config := runTunnelConfigDialog(tp.Form(), nil); config != nil {
+ // Save new
+ tp.addTunnel(config)
+ }
+}
+
+func (tp *TunnelsPage) onDelete() {
+ currentTunnel := tp.tunnelsView.CurrentTunnel()
+ if currentTunnel == nil {
+ // Misfired event?
+ return
+ }
+
+ if walk.DlgCmdNo == walk.MsgBox(
+ tp.Form(),
+ fmt.Sprintf(`Delete "%s"`, currentTunnel.Name),
+ fmt.Sprintf(`Are you sure you want to delete "%s"?`, currentTunnel.Name),
+ walk.MsgBoxYesNo|walk.MsgBoxIconWarning) {
+ return
+ }
+
+ tp.deleteTunnel(currentTunnel)
+
+ tp.tunnelDeletedPublisher.Publish(currentTunnel.Name)
+}
+
+func (tp *TunnelsPage) onImport() {
+ dlg := walk.FileDialog{
+ Filter: "Configuration Files (*.zip, *.conf)|*.zip;*.conf|All Files (*.*)|*.*",
+ Title: "Import tunnel(s) from file...",
+ }
+
+ if ok, _ := dlg.ShowOpenMultiple(tp.Form()); !ok {
+ return
+ }
+
+ tp.importFiles(dlg.FilePaths)
+}
+
+func (tp *TunnelsPage) onExportTunnels() {
+ dlg := walk.FileDialog{
+ Filter: "Configuration ZIP Files (*.zip)|*.zip",
+ Title: "Export tunnels to zip...",
+ }
+
+ if ok, _ := dlg.ShowSave(tp.Form()); !ok {
+ return
+ }
+
+ if !strings.HasSuffix(dlg.FilePath, ".zip") {
+ dlg.FilePath += ".zip"
+ }
+
+ tp.exportTunnels(dlg.FilePath)
+}