From 48f11e502cc7ee03a9c1dd554c3665883fc90cff Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Wed, 24 Apr 2019 15:29:38 +0200 Subject: 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 Signed-off-by: Jason A. Donenfeld --- ui/tunnelspage.go | 403 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 403 insertions(+) create mode 100644 ui/tunnelspage.go (limited to 'ui/tunnelspage.go') 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) +} -- cgit v1.2.3-59-g8ed1b