From 48098ab608248a613da960d64dad4a653194aa41 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Thu, 2 May 2019 18:32:04 +0200 Subject: ui: rename things to say tunnel less --- ui/editdialog.go | 323 +++++++++++++++++++++++++++++++++++++++++++++++++ ui/listview.go | 227 ++++++++++++++++++++++++++++++++++ ui/tunneleditdialog.go | 323 ------------------------------------------------- ui/tunnelspage.go | 2 +- ui/tunnelsview.go | 227 ---------------------------------- 5 files changed, 551 insertions(+), 551 deletions(-) create mode 100644 ui/editdialog.go create mode 100644 ui/listview.go delete mode 100644 ui/tunneleditdialog.go delete mode 100644 ui/tunnelsview.go diff --git a/ui/editdialog.go b/ui/editdialog.go new file mode 100644 index 00000000..3cd5b606 --- /dev/null +++ b/ui/editdialog.go @@ -0,0 +1,323 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package ui + +import ( + "fmt" + "strings" + + "github.com/lxn/walk" + "golang.zx2c4.com/wireguard/windows/conf" + "golang.zx2c4.com/wireguard/windows/service" + "golang.zx2c4.com/wireguard/windows/ui/syntax" +) + +const ( + configKeyDNS = "DNS" + configKeyAllowedIPs = "AllowedIPs" +) + +var ( + ipv4Wildcard = orderedStringSetFromSlice([]string{"0.0.0.0/0"}) + ipv4PublicNetworks = orderedStringSetFromSlice([]string{ + "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", + "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", + "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", + "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", + "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", + "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4", + }) +) + +type allowedIPsState int + +const ( + allowedIPsStateInvalid allowedIPsState = iota + allowedIPsStateContainsIPV4Wildcard + allowedIPsStateContainsIPV4PublicNetworks + allowedIPsStateOther +) + +type EditDialog struct { + *walk.Dialog + nameEdit *walk.LineEdit + pubkeyEdit *walk.LineEdit + syntaxEdit *syntax.SyntaxEdit + excludePrivateIPsCB *walk.CheckBox + saveButton *walk.PushButton + tunnel *service.Tunnel + config conf.Config + allowedIPsState allowedIPsState + lastPrivateKey string + inCheckedChanged bool +} + +func runTunnelEditDialog(owner walk.Form, tunnel *service.Tunnel) *conf.Config { + var ( + title string + name string + ) + + dlg := &EditDialog{tunnel: tunnel} + + if tunnel == nil { + // Creating a new tunnel, create a new private key and use the default template + title = "Create new tunnel" + pk, _ := conf.NewPrivateKey() + dlg.config = conf.Config{Interface: conf.Interface{PrivateKey: *pk}} + } else { + title = "Edit tunnel" + name = tunnel.Name + dlg.config, _ = tunnel.StoredConfig() + } + + layout := walk.NewGridLayout() + layout.SetSpacing(6) + layout.SetMargins(walk.Margins{10, 10, 10, 10}) + layout.SetColumnStretchFactor(1, 3) + + dlg.Dialog, _ = walk.NewDialog(owner) + dlg.SetIcon(owner.Icon()) + dlg.SetTitle(title) + dlg.SetLayout(layout) + dlg.SetMinMaxSize(walk.Size{500, 400}, walk.Size{0, 0}) + + nameLabel, _ := walk.NewTextLabel(dlg) + layout.SetRange(nameLabel, walk.Rectangle{0, 0, 1, 1}) + nameLabel.SetTextAlignment(walk.AlignHFarVCenter) + nameLabel.SetText("Name:") + + dlg.nameEdit, _ = walk.NewLineEdit(dlg) + layout.SetRange(dlg.nameEdit, walk.Rectangle{1, 0, 1, 1}) + dlg.nameEdit.SetText(name) + + pubkeyLabel, _ := walk.NewTextLabel(dlg) + layout.SetRange(pubkeyLabel, walk.Rectangle{0, 1, 1, 1}) + pubkeyLabel.SetTextAlignment(walk.AlignHFarVCenter) + pubkeyLabel.SetText("Public key:") + + dlg.pubkeyEdit, _ = walk.NewLineEdit(dlg) + layout.SetRange(dlg.pubkeyEdit, walk.Rectangle{1, 1, 1, 1}) + dlg.pubkeyEdit.SetReadOnly(true) + dlg.pubkeyEdit.SetText("(unknown)") + + dlg.syntaxEdit, _ = syntax.NewSyntaxEdit(dlg) + layout.SetRange(dlg.syntaxEdit, walk.Rectangle{0, 2, 2, 1}) + dlg.syntaxEdit.PrivateKeyChanged().Attach(dlg.onSyntaxEditPrivateKeyChanged) + dlg.syntaxEdit.SetText(dlg.config.ToWgQuick()) + dlg.syntaxEdit.TextChanged().Attach(dlg.updateExcludePrivateIPsCBVisible) + + buttonsContainer, _ := walk.NewComposite(dlg) + layout.SetRange(buttonsContainer, walk.Rectangle{0, 3, 2, 1}) + buttonsContainer.SetLayout(walk.NewHBoxLayout()) + buttonsContainer.Layout().SetMargins(walk.Margins{}) + + dlg.excludePrivateIPsCB, _ = walk.NewCheckBox(buttonsContainer) + dlg.excludePrivateIPsCB.SetText("Exclude private IPs") + dlg.excludePrivateIPsCB.CheckedChanged().Attach(dlg.onExcludePrivateIPsCBCheckedChanged) + dlg.updateExcludePrivateIPsCBVisible() + + walk.NewHSpacer(buttonsContainer) + + dlg.saveButton, _ = walk.NewPushButton(buttonsContainer) + dlg.saveButton.SetText("Save") + dlg.saveButton.Clicked().Attach(dlg.onSaveButtonClicked) + + cancelButton, _ := walk.NewPushButton(buttonsContainer) + cancelButton.SetText("Cancel") + cancelButton.Clicked().Attach(dlg.Cancel) + + dlg.SetCancelButton(cancelButton) + dlg.SetDefaultButton(dlg.saveButton) + + dlg.updateAllowedIPsState() + + if dlg.Run() == walk.DlgCmdOK { + // Save + return &dlg.config + } + + return nil +} + +func (dlg *EditDialog) updateAllowedIPsState() { + var newState allowedIPsState + if len(dlg.config.Peers) == 1 { + if allowedIPs := dlg.allowedIPsSet(); allowedIPs.IsSupersetOf(ipv4Wildcard) { + newState = allowedIPsStateContainsIPV4Wildcard + } else if allowedIPs.IsSupersetOf(ipv4PublicNetworks) { + newState = allowedIPsStateContainsIPV4PublicNetworks + } else { + newState = allowedIPsStateOther + } + } else { + newState = allowedIPsStateInvalid + } + + if newState != dlg.allowedIPsState { + dlg.allowedIPsState = newState + + dlg.excludePrivateIPsCB.SetVisible(dlg.canExcludePrivateIPs()) + dlg.excludePrivateIPsCB.SetChecked(dlg.privateIPsExcluded()) + } +} + +func (dlg *EditDialog) canExcludePrivateIPs() bool { + return dlg.allowedIPsState == allowedIPsStateContainsIPV4PublicNetworks || + dlg.allowedIPsState == allowedIPsStateContainsIPV4Wildcard +} + +func (dlg *EditDialog) privateIPsExcluded() bool { + return dlg.allowedIPsState == allowedIPsStateContainsIPV4PublicNetworks +} + +func (dlg *EditDialog) setPrivateIPsExcluded(excluded bool) { + if !dlg.canExcludePrivateIPs() || dlg.privateIPsExcluded() == excluded { + return + } + + var oldNetworks, newNetworks *orderedStringSet + if excluded { + oldNetworks, newNetworks = ipv4Wildcard, ipv4PublicNetworks + } else { + oldNetworks, newNetworks = ipv4PublicNetworks, ipv4Wildcard + } + input := dlg.allowedIPs() + output := newOrderedStringSet() + var replaced bool + + // Replace the first instance of the wildcard with the public network list, or vice versa. + for _, network := range input { + if oldNetworks.Contains(network) { + if !replaced { + output.UniteWith(newNetworks) + replaced = true + } + } else { + output.Add(network) + } + } + + // DNS servers only need to be handled specially when we're excluding private IPs. + for _, route := range dlg.dnsRoutes() { + if excluded { + output.Add(route) + } else { + output.Remove(route) + output.Remove(route + "/32") + } + } + + if excluded { + dlg.allowedIPsState = allowedIPsStateContainsIPV4PublicNetworks + } else { + dlg.allowedIPsState = allowedIPsStateContainsIPV4Wildcard + } + + dlg.replaceLine(configKeyAllowedIPs, strings.Join(output.ToSlice(), ", ")) +} + +func (dlg *EditDialog) replaceLine(key, value string) { + text := dlg.syntaxEdit.Text() + + start := strings.Index(text, key) + end := start + strings.Index(text[start:], "\n") + oldLine := text[start:end] + newLine := fmt.Sprintf("%s = %s", key, value) + + dlg.syntaxEdit.SetText(strings.ReplaceAll(text, oldLine, newLine)) +} + +func (dlg *EditDialog) updateExcludePrivateIPsCBVisible() { + dlg.updateAllowedIPsState() + + dlg.excludePrivateIPsCB.SetVisible(dlg.canExcludePrivateIPs()) +} + +func (dlg *EditDialog) dnsRoutes() []string { + return dlg.routes(configKeyDNS) +} + +func (dlg *EditDialog) allowedIPs() []string { + return dlg.routes(configKeyAllowedIPs) +} + +func (dlg *EditDialog) allowedIPsSet() *orderedStringSet { + return orderedStringSetFromSlice(dlg.allowedIPs()) +} + +func (dlg *EditDialog) routes(key string) []string { + var routes []string + + lines := strings.Split(dlg.syntaxEdit.Text(), "\n") + for _, line := range lines { + if strings.HasPrefix(strings.TrimSpace(line), key) { + routesMaybeWithSpace := strings.Split(strings.TrimSpace(line[strings.IndexByte(line, '=')+1:]), ",") + routes = make([]string, len(routesMaybeWithSpace)) + for i, route := range routesMaybeWithSpace { + routes[i] = strings.TrimSpace(route) + } + break + } + } + + return routes +} + +func (dlg *EditDialog) onExcludePrivateIPsCBCheckedChanged() { + dlg.setPrivateIPsExcluded(dlg.excludePrivateIPsCB.Checked()) +} + +func (dlg *EditDialog) onSyntaxEditPrivateKeyChanged(privateKey string) { + if privateKey == dlg.lastPrivateKey { + return + } + dlg.lastPrivateKey = privateKey + key, _ := conf.NewPrivateKeyFromString(privateKey) + if key != nil { + dlg.pubkeyEdit.SetText(key.Public().String()) + } else { + dlg.pubkeyEdit.SetText("(unknown)") + } +} + +func (dlg *EditDialog) onSaveButtonClicked() { + newName := dlg.nameEdit.Text() + if newName == "" { + walk.MsgBox(dlg, "Invalid configuration", "Name is required", walk.MsgBoxIconWarning) + return + } + + if dlg.tunnel != nil && dlg.tunnel.Name != newName { + names, err := conf.ListConfigNames() + if err != nil { + walk.MsgBox(dlg, "Error", err.Error(), walk.MsgBoxIconError) + return + } + + for _, name := range names { + if strings.ToLower(name) == strings.ToLower(newName) { + walk.MsgBox(dlg, "Invalid configuration", fmt.Sprintf("Another tunnel already exists with the name ‘%s’.", newName), walk.MsgBoxIconWarning) + return + } + } + } + + if !conf.TunnelNameIsValid(newName) { + walk.MsgBox(dlg, "Invalid configuration", fmt.Sprintf("Tunnel name ‘%s’ is invalid.", newName), walk.MsgBoxIconWarning) + return + } + + cfg, err := conf.FromWgQuick(dlg.syntaxEdit.Text(), newName) + if err != nil { + walk.MsgBox(dlg, "Error", err.Error(), walk.MsgBoxIconError) + return + } + + dlg.config = *cfg + + dlg.Accept() +} diff --git a/ui/listview.go b/ui/listview.go new file mode 100644 index 00000000..d9ef057c --- /dev/null +++ b/ui/listview.go @@ -0,0 +1,227 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package ui + +import ( + "sort" + "strings" + + "github.com/lxn/walk" + "golang.zx2c4.com/wireguard/windows/service" +) + +// ListModel is a struct to store the currently known tunnels to the GUI, suitable as a model for a walk.TableView. +type ListModel struct { + walk.TableModelBase + walk.SorterBase + + tunnels []service.Tunnel +} + +func (t *ListModel) RowCount() int { + return len(t.tunnels) +} + +func (t *ListModel) Value(row, col int) interface{} { + tunnel := t.tunnels[row] + + switch col { + case 0: + return tunnel.Name + + default: + panic("unreachable col") + } +} + +func (t *ListModel) Sort(col int, order walk.SortOrder) error { + sort.SliceStable(t.tunnels, func(i, j int) bool { + //TODO: use real string comparison for sorting with proper tunnel order + return t.tunnels[i].Name < t.tunnels[j].Name + }) + + return t.SorterBase.Sort(col, order) +} + +type ListView struct { + *walk.TableView + + model *ListModel + + tunnelChangedCB *service.TunnelChangeCallback + tunnelsChangedCB *service.TunnelsChangeCallback +} + +func NewTunnelsView(parent walk.Container) (*ListView, error) { + var disposables walk.Disposables + defer disposables.Treat() + + tv, err := walk.NewTableView(parent) + if err != nil { + return nil, err + } + disposables.Add(tv) + + model := new(ListModel) + if model.tunnels, err = service.IPCClientTunnels(); err != nil { + return nil, err + } + + tv.SetModel(model) + tv.SetLastColumnStretched(true) + tv.SetHeaderHidden(true) + tv.Columns().Add(walk.NewTableViewColumn()) + + tunnelsView := &ListView{ + TableView: tv, + model: model, + } + + tv.SetCellStyler(tunnelsView) + + disposables.Spare() + + tunnelsView.tunnelChangedCB = service.IPCClientRegisterTunnelChange(tunnelsView.onTunnelChange) + tunnelsView.tunnelsChangedCB = service.IPCClientRegisterTunnelsChange(tunnelsView.onTunnelsChange) + tunnelsView.onTunnelsChange() + + return tunnelsView, nil +} + +func (tv *ListView) Dispose() { + if tv.tunnelChangedCB != nil { + tv.tunnelChangedCB.Unregister() + tv.tunnelChangedCB = nil + } + if tv.tunnelsChangedCB != nil { + tv.tunnelsChangedCB.Unregister() + tv.tunnelsChangedCB = nil + } + tv.TableView.Dispose() +} + +func (tv *ListView) StyleCell(style *walk.CellStyle) { + canvas := style.Canvas() + if canvas == nil { + return + } + + tunnel := &tv.model.tunnels[style.Row()] + + b := style.Bounds() + + b.X = b.Height + b.Width -= b.Height + canvas.DrawText(tunnel.Name, tv.Font(), 0, b, walk.TextVCenter|walk.TextSingleLine) + + b.X = 0 + b.Width = b.Height + + iconProvider.PaintForTunnel(tunnel, canvas, b) +} + +func (tv *ListView) CurrentTunnel() *service.Tunnel { + idx := tv.CurrentIndex() + if idx == -1 { + return nil + } + + return &tv.model.tunnels[idx] +} + +func (tv *ListView) onTunnelChange(tunnel *service.Tunnel, state service.TunnelState, globalState service.TunnelState, err error) { + tv.Synchronize(func() { + idx := -1 + for i := range tv.model.tunnels { + if tv.model.tunnels[i].Name == tunnel.Name { + idx = i + break + } + } + + if idx != -1 { + tv.model.PublishRowChanged(idx) + return + } + }) +} + +func (tv *ListView) onTunnelsChange() { + tunnels, err := service.IPCClientTunnels() + if err != nil { + return + } + tv.Synchronize(func() { + newTunnels := make(map[service.Tunnel]bool, len(tunnels)) + oldTunnels := make(map[service.Tunnel]bool, len(tv.model.tunnels)) + for _, tunnel := range tunnels { + newTunnels[tunnel] = true + } + for _, tunnel := range tv.model.tunnels { + oldTunnels[tunnel] = true + } + + for tunnel := range oldTunnels { + if !newTunnels[tunnel] { + for i, t := range tv.model.tunnels { + //TODO: this is inefficient. Use a map here instead. + if t.Name == tunnel.Name { + tv.model.tunnels = append(tv.model.tunnels[:i], tv.model.tunnels[i+1:]...) + tv.model.PublishRowsRemoved(i, i) + break + } + } + } + } + didAdd := false + firstTunnelName := "" + for tunnel := range newTunnels { + if !oldTunnels[tunnel] { + //TODO: use proper tunnel string sorting/comparison algorithm, as the other comments indicate too. + if len(firstTunnelName) == 0 || strings.Compare(firstTunnelName, tunnel.Name) > 0 { + firstTunnelName = tunnel.Name + } + tv.model.tunnels = append(tv.model.tunnels, tunnel) + didAdd = true + } + } + if didAdd { + tv.model.PublishRowsReset() + tv.model.Sort(tv.model.SortedColumn(), tv.model.SortOrder()) + if len(tv.SelectedIndexes()) == 0 { + tv.selectTunnel(firstTunnelName) + } + } + }) +} + +func (tv *ListView) selectTunnel(tunnelName string) { + for i, tunnel := range tv.model.tunnels { + if tunnel.Name == tunnelName { + tv.SetCurrentIndex(i) + break + } + } +} + +func (tv *ListView) SelectFirstActiveTunnel() { + tunnels := make([]service.Tunnel, len(tv.model.tunnels)) + copy(tunnels, tv.model.tunnels) + go func() { + for _, tunnel := range tunnels { + state, err := tunnel.State() + if err != nil { + continue + } + if state == service.TunnelStarting || state == service.TunnelStarted { + tv.Synchronize(func() { + tv.selectTunnel(tunnel.Name) + }) + return + } + } + }() +} diff --git a/ui/tunneleditdialog.go b/ui/tunneleditdialog.go deleted file mode 100644 index 6f5ca8ab..00000000 --- a/ui/tunneleditdialog.go +++ /dev/null @@ -1,323 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. - */ - -package ui - -import ( - "fmt" - "strings" - - "github.com/lxn/walk" - "golang.zx2c4.com/wireguard/windows/conf" - "golang.zx2c4.com/wireguard/windows/service" - "golang.zx2c4.com/wireguard/windows/ui/syntax" -) - -const ( - configKeyDNS = "DNS" - configKeyAllowedIPs = "AllowedIPs" -) - -var ( - ipv4Wildcard = orderedStringSetFromSlice([]string{"0.0.0.0/0"}) - ipv4PublicNetworks = orderedStringSetFromSlice([]string{ - "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", - "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", - "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", - "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", - "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", - "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4", - }) -) - -type allowedIPsState int - -const ( - allowedIPsStateInvalid allowedIPsState = iota - allowedIPsStateContainsIPV4Wildcard - allowedIPsStateContainsIPV4PublicNetworks - allowedIPsStateOther -) - -type TunnelEditDialog struct { - *walk.Dialog - nameEdit *walk.LineEdit - pubkeyEdit *walk.LineEdit - syntaxEdit *syntax.SyntaxEdit - excludePrivateIPsCB *walk.CheckBox - saveButton *walk.PushButton - tunnel *service.Tunnel - config conf.Config - allowedIPsState allowedIPsState - lastPrivateKey string - inCheckedChanged bool -} - -func runTunnelEditDialog(owner walk.Form, tunnel *service.Tunnel) *conf.Config { - var ( - title string - name string - ) - - dlg := &TunnelEditDialog{tunnel: tunnel} - - if tunnel == nil { - // Creating a new tunnel, create a new private key and use the default template - title = "Create new tunnel" - pk, _ := conf.NewPrivateKey() - dlg.config = conf.Config{Interface: conf.Interface{PrivateKey: *pk}} - } else { - title = "Edit tunnel" - name = tunnel.Name - dlg.config, _ = tunnel.StoredConfig() - } - - layout := walk.NewGridLayout() - layout.SetSpacing(6) - layout.SetMargins(walk.Margins{10, 10, 10, 10}) - layout.SetColumnStretchFactor(1, 3) - - dlg.Dialog, _ = walk.NewDialog(owner) - dlg.SetIcon(owner.Icon()) - dlg.SetTitle(title) - dlg.SetLayout(layout) - dlg.SetMinMaxSize(walk.Size{500, 400}, walk.Size{0, 0}) - - nameLabel, _ := walk.NewTextLabel(dlg) - layout.SetRange(nameLabel, walk.Rectangle{0, 0, 1, 1}) - nameLabel.SetTextAlignment(walk.AlignHFarVCenter) - nameLabel.SetText("Name:") - - dlg.nameEdit, _ = walk.NewLineEdit(dlg) - layout.SetRange(dlg.nameEdit, walk.Rectangle{1, 0, 1, 1}) - dlg.nameEdit.SetText(name) - - pubkeyLabel, _ := walk.NewTextLabel(dlg) - layout.SetRange(pubkeyLabel, walk.Rectangle{0, 1, 1, 1}) - pubkeyLabel.SetTextAlignment(walk.AlignHFarVCenter) - pubkeyLabel.SetText("Public key:") - - dlg.pubkeyEdit, _ = walk.NewLineEdit(dlg) - layout.SetRange(dlg.pubkeyEdit, walk.Rectangle{1, 1, 1, 1}) - dlg.pubkeyEdit.SetReadOnly(true) - dlg.pubkeyEdit.SetText("(unknown)") - - dlg.syntaxEdit, _ = syntax.NewSyntaxEdit(dlg) - layout.SetRange(dlg.syntaxEdit, walk.Rectangle{0, 2, 2, 1}) - dlg.syntaxEdit.PrivateKeyChanged().Attach(dlg.onSyntaxEditPrivateKeyChanged) - dlg.syntaxEdit.SetText(dlg.config.ToWgQuick()) - dlg.syntaxEdit.TextChanged().Attach(dlg.updateExcludePrivateIPsCBVisible) - - buttonsContainer, _ := walk.NewComposite(dlg) - layout.SetRange(buttonsContainer, walk.Rectangle{0, 3, 2, 1}) - buttonsContainer.SetLayout(walk.NewHBoxLayout()) - buttonsContainer.Layout().SetMargins(walk.Margins{}) - - dlg.excludePrivateIPsCB, _ = walk.NewCheckBox(buttonsContainer) - dlg.excludePrivateIPsCB.SetText("Exclude private IPs") - dlg.excludePrivateIPsCB.CheckedChanged().Attach(dlg.onExcludePrivateIPsCBCheckedChanged) - dlg.updateExcludePrivateIPsCBVisible() - - walk.NewHSpacer(buttonsContainer) - - dlg.saveButton, _ = walk.NewPushButton(buttonsContainer) - dlg.saveButton.SetText("Save") - dlg.saveButton.Clicked().Attach(dlg.onSaveButtonClicked) - - cancelButton, _ := walk.NewPushButton(buttonsContainer) - cancelButton.SetText("Cancel") - cancelButton.Clicked().Attach(dlg.Cancel) - - dlg.SetCancelButton(cancelButton) - dlg.SetDefaultButton(dlg.saveButton) - - dlg.updateAllowedIPsState() - - if dlg.Run() == walk.DlgCmdOK { - // Save - return &dlg.config - } - - return nil -} - -func (dlg *TunnelEditDialog) updateAllowedIPsState() { - var newState allowedIPsState - if len(dlg.config.Peers) == 1 { - if allowedIPs := dlg.allowedIPsSet(); allowedIPs.IsSupersetOf(ipv4Wildcard) { - newState = allowedIPsStateContainsIPV4Wildcard - } else if allowedIPs.IsSupersetOf(ipv4PublicNetworks) { - newState = allowedIPsStateContainsIPV4PublicNetworks - } else { - newState = allowedIPsStateOther - } - } else { - newState = allowedIPsStateInvalid - } - - if newState != dlg.allowedIPsState { - dlg.allowedIPsState = newState - - dlg.excludePrivateIPsCB.SetVisible(dlg.canExcludePrivateIPs()) - dlg.excludePrivateIPsCB.SetChecked(dlg.privateIPsExcluded()) - } -} - -func (dlg *TunnelEditDialog) canExcludePrivateIPs() bool { - return dlg.allowedIPsState == allowedIPsStateContainsIPV4PublicNetworks || - dlg.allowedIPsState == allowedIPsStateContainsIPV4Wildcard -} - -func (dlg *TunnelEditDialog) privateIPsExcluded() bool { - return dlg.allowedIPsState == allowedIPsStateContainsIPV4PublicNetworks -} - -func (dlg *TunnelEditDialog) setPrivateIPsExcluded(excluded bool) { - if !dlg.canExcludePrivateIPs() || dlg.privateIPsExcluded() == excluded { - return - } - - var oldNetworks, newNetworks *orderedStringSet - if excluded { - oldNetworks, newNetworks = ipv4Wildcard, ipv4PublicNetworks - } else { - oldNetworks, newNetworks = ipv4PublicNetworks, ipv4Wildcard - } - input := dlg.allowedIPs() - output := newOrderedStringSet() - var replaced bool - - // Replace the first instance of the wildcard with the public network list, or vice versa. - for _, network := range input { - if oldNetworks.Contains(network) { - if !replaced { - output.UniteWith(newNetworks) - replaced = true - } - } else { - output.Add(network) - } - } - - // DNS servers only need to be handled specially when we're excluding private IPs. - for _, route := range dlg.dnsRoutes() { - if excluded { - output.Add(route) - } else { - output.Remove(route) - output.Remove(route + "/32") - } - } - - if excluded { - dlg.allowedIPsState = allowedIPsStateContainsIPV4PublicNetworks - } else { - dlg.allowedIPsState = allowedIPsStateContainsIPV4Wildcard - } - - dlg.replaceLine(configKeyAllowedIPs, strings.Join(output.ToSlice(), ", ")) -} - -func (dlg *TunnelEditDialog) replaceLine(key, value string) { - text := dlg.syntaxEdit.Text() - - start := strings.Index(text, key) - end := start + strings.Index(text[start:], "\n") - oldLine := text[start:end] - newLine := fmt.Sprintf("%s = %s", key, value) - - dlg.syntaxEdit.SetText(strings.ReplaceAll(text, oldLine, newLine)) -} - -func (dlg *TunnelEditDialog) updateExcludePrivateIPsCBVisible() { - dlg.updateAllowedIPsState() - - dlg.excludePrivateIPsCB.SetVisible(dlg.canExcludePrivateIPs()) -} - -func (dlg *TunnelEditDialog) dnsRoutes() []string { - return dlg.routes(configKeyDNS) -} - -func (dlg *TunnelEditDialog) allowedIPs() []string { - return dlg.routes(configKeyAllowedIPs) -} - -func (dlg *TunnelEditDialog) allowedIPsSet() *orderedStringSet { - return orderedStringSetFromSlice(dlg.allowedIPs()) -} - -func (dlg *TunnelEditDialog) routes(key string) []string { - var routes []string - - lines := strings.Split(dlg.syntaxEdit.Text(), "\n") - for _, line := range lines { - if strings.HasPrefix(strings.TrimSpace(line), key) { - routesMaybeWithSpace := strings.Split(strings.TrimSpace(line[strings.IndexByte(line, '=')+1:]), ",") - routes = make([]string, len(routesMaybeWithSpace)) - for i, route := range routesMaybeWithSpace { - routes[i] = strings.TrimSpace(route) - } - break - } - } - - return routes -} - -func (dlg *TunnelEditDialog) onExcludePrivateIPsCBCheckedChanged() { - dlg.setPrivateIPsExcluded(dlg.excludePrivateIPsCB.Checked()) -} - -func (dlg *TunnelEditDialog) onSyntaxEditPrivateKeyChanged(privateKey string) { - if privateKey == dlg.lastPrivateKey { - return - } - dlg.lastPrivateKey = privateKey - key, _ := conf.NewPrivateKeyFromString(privateKey) - if key != nil { - dlg.pubkeyEdit.SetText(key.Public().String()) - } else { - dlg.pubkeyEdit.SetText("(unknown)") - } -} - -func (dlg *TunnelEditDialog) onSaveButtonClicked() { - newName := dlg.nameEdit.Text() - if newName == "" { - walk.MsgBox(dlg, "Invalid configuration", "Name is required", walk.MsgBoxIconWarning) - return - } - - if dlg.tunnel != nil && dlg.tunnel.Name != newName { - names, err := conf.ListConfigNames() - if err != nil { - walk.MsgBox(dlg, "Error", err.Error(), walk.MsgBoxIconError) - return - } - - for _, name := range names { - if strings.ToLower(name) == strings.ToLower(newName) { - walk.MsgBox(dlg, "Invalid configuration", fmt.Sprintf("Another tunnel already exists with the name ‘%s’.", newName), walk.MsgBoxIconWarning) - return - } - } - } - - if !conf.TunnelNameIsValid(newName) { - walk.MsgBox(dlg, "Invalid configuration", fmt.Sprintf("Tunnel name ‘%s’ is invalid.", newName), walk.MsgBoxIconWarning) - return - } - - cfg, err := conf.FromWgQuick(dlg.syntaxEdit.Text(), newName) - if err != nil { - walk.MsgBox(dlg, "Error", err.Error(), walk.MsgBoxIconError) - return - } - - dlg.config = *cfg - - dlg.Accept() -} diff --git a/ui/tunnelspage.go b/ui/tunnelspage.go index 8cb64904..10a6c782 100644 --- a/ui/tunnelspage.go +++ b/ui/tunnelspage.go @@ -23,7 +23,7 @@ import ( type TunnelsPage struct { *walk.TabPage - tunnelsView *TunnelsView + tunnelsView *ListView confView *ConfView fillerButton *walk.PushButton fillerHandler func() diff --git a/ui/tunnelsview.go b/ui/tunnelsview.go deleted file mode 100644 index c2ce7946..00000000 --- a/ui/tunnelsview.go +++ /dev/null @@ -1,227 +0,0 @@ -/* SPDX-License-Identifier: MIT - * - * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. - */ - -package ui - -import ( - "sort" - "strings" - - "github.com/lxn/walk" - "golang.zx2c4.com/wireguard/windows/service" -) - -// TunnelModel is a struct to store the currently known tunnels to the GUI, suitable as a model for a walk.TableView. -type TunnelModel struct { - walk.TableModelBase - walk.SorterBase - - tunnels []service.Tunnel -} - -func (t *TunnelModel) RowCount() int { - return len(t.tunnels) -} - -func (t *TunnelModel) Value(row, col int) interface{} { - tunnel := t.tunnels[row] - - switch col { - case 0: - return tunnel.Name - - default: - panic("unreachable col") - } -} - -func (t *TunnelModel) Sort(col int, order walk.SortOrder) error { - sort.SliceStable(t.tunnels, func(i, j int) bool { - //TODO: use real string comparison for sorting with proper tunnel order - return t.tunnels[i].Name < t.tunnels[j].Name - }) - - return t.SorterBase.Sort(col, order) -} - -type TunnelsView struct { - *walk.TableView - - model *TunnelModel - - tunnelChangedCB *service.TunnelChangeCallback - tunnelsChangedCB *service.TunnelsChangeCallback -} - -func NewTunnelsView(parent walk.Container) (*TunnelsView, error) { - var disposables walk.Disposables - defer disposables.Treat() - - tv, err := walk.NewTableView(parent) - if err != nil { - return nil, err - } - disposables.Add(tv) - - model := new(TunnelModel) - if model.tunnels, err = service.IPCClientTunnels(); err != nil { - return nil, err - } - - tv.SetModel(model) - tv.SetLastColumnStretched(true) - tv.SetHeaderHidden(true) - tv.Columns().Add(walk.NewTableViewColumn()) - - tunnelsView := &TunnelsView{ - TableView: tv, - model: model, - } - - tv.SetCellStyler(tunnelsView) - - disposables.Spare() - - tunnelsView.tunnelChangedCB = service.IPCClientRegisterTunnelChange(tunnelsView.onTunnelChange) - tunnelsView.tunnelsChangedCB = service.IPCClientRegisterTunnelsChange(tunnelsView.onTunnelsChange) - tunnelsView.onTunnelsChange() - - return tunnelsView, nil -} - -func (tv *TunnelsView) Dispose() { - if tv.tunnelChangedCB != nil { - tv.tunnelChangedCB.Unregister() - tv.tunnelChangedCB = nil - } - if tv.tunnelsChangedCB != nil { - tv.tunnelsChangedCB.Unregister() - tv.tunnelsChangedCB = nil - } - tv.TableView.Dispose() -} - -func (tv *TunnelsView) StyleCell(style *walk.CellStyle) { - canvas := style.Canvas() - if canvas == nil { - return - } - - tunnel := &tv.model.tunnels[style.Row()] - - b := style.Bounds() - - b.X = b.Height - b.Width -= b.Height - canvas.DrawText(tunnel.Name, tv.Font(), 0, b, walk.TextVCenter|walk.TextSingleLine) - - b.X = 0 - b.Width = b.Height - - iconProvider.PaintForTunnel(tunnel, canvas, b) -} - -func (tv *TunnelsView) CurrentTunnel() *service.Tunnel { - idx := tv.CurrentIndex() - if idx == -1 { - return nil - } - - return &tv.model.tunnels[idx] -} - -func (tv *TunnelsView) onTunnelChange(tunnel *service.Tunnel, state service.TunnelState, globalState service.TunnelState, err error) { - tv.Synchronize(func() { - idx := -1 - for i := range tv.model.tunnels { - if tv.model.tunnels[i].Name == tunnel.Name { - idx = i - break - } - } - - if idx != -1 { - tv.model.PublishRowChanged(idx) - return - } - }) -} - -func (tv *TunnelsView) onTunnelsChange() { - tunnels, err := service.IPCClientTunnels() - if err != nil { - return - } - tv.Synchronize(func() { - newTunnels := make(map[service.Tunnel]bool, len(tunnels)) - oldTunnels := make(map[service.Tunnel]bool, len(tv.model.tunnels)) - for _, tunnel := range tunnels { - newTunnels[tunnel] = true - } - for _, tunnel := range tv.model.tunnels { - oldTunnels[tunnel] = true - } - - for tunnel := range oldTunnels { - if !newTunnels[tunnel] { - for i, t := range tv.model.tunnels { - //TODO: this is inefficient. Use a map here instead. - if t.Name == tunnel.Name { - tv.model.tunnels = append(tv.model.tunnels[:i], tv.model.tunnels[i+1:]...) - tv.model.PublishRowsRemoved(i, i) - break - } - } - } - } - didAdd := false - firstTunnelName := "" - for tunnel := range newTunnels { - if !oldTunnels[tunnel] { - //TODO: use proper tunnel string sorting/comparison algorithm, as the other comments indicate too. - if len(firstTunnelName) == 0 || strings.Compare(firstTunnelName, tunnel.Name) > 0 { - firstTunnelName = tunnel.Name - } - tv.model.tunnels = append(tv.model.tunnels, tunnel) - didAdd = true - } - } - if didAdd { - tv.model.PublishRowsReset() - tv.model.Sort(tv.model.SortedColumn(), tv.model.SortOrder()) - if len(tv.SelectedIndexes()) == 0 { - tv.selectTunnel(firstTunnelName) - } - } - }) -} - -func (tv *TunnelsView) selectTunnel(tunnelName string) { - for i, tunnel := range tv.model.tunnels { - if tunnel.Name == tunnelName { - tv.SetCurrentIndex(i) - break - } - } -} - -func (tv *TunnelsView) SelectFirstActiveTunnel() { - tunnels := make([]service.Tunnel, len(tv.model.tunnels)) - copy(tunnels, tv.model.tunnels) - go func() { - for _, tunnel := range tunnels { - state, err := tunnel.State() - if err != nil { - continue - } - if state == service.TunnelStarting || state == service.TunnelStarted { - tv.Synchronize(func() { - tv.selectTunnel(tunnel.Name) - }) - return - } - } - }() -} -- cgit v1.2.3-59-g8ed1b