diff --git a/fw-settings/builder.go b/fw-settings/builder.go
new file mode 100644
index 0000000..05a321d
--- /dev/null
+++ b/fw-settings/builder.go
@@ -0,0 +1,114 @@
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "reflect"
+
+ "github.com/gotk3/gotk3/glib"
+ "github.com/gotk3/gotk3/gtk"
+ "github.com/subgraph/fw-daemon/fw-settings/definitions"
+)
+
+const (
+ defsFolder = "definitions"
+ xmlExtension = ".xml"
+)
+
+func getDefinitionWithFileFallback(uiName string) string {
+ // this makes sure a missing definition wont break only when the app is released
+ uiDef := getDefinition(uiName)
+
+ fileName := filepath.Join(defsFolder, uiName+xmlExtension)
+ if fileNotFound(fileName) {
+ log.Printf("gui: loading compiled definition %q\n", uiName)
+ return uiDef.String()
+ }
+
+ return readFile(fileName)
+}
+
+// This must be called from the UI thread - otherwise bad things will happen sooner or later
+func builderForDefinition(uiName string) *gtk.Builder {
+ // assertInUIThread()
+
+ template := getDefinitionWithFileFallback(uiName)
+
+ builder, err := gtk.BuilderNew()
+ if err != nil {
+ //We cant recover from this
+ panic(err)
+ }
+
+ err = builder.AddFromString(template)
+ if err != nil {
+ //This is a programming error
+ panic(fmt.Sprintf("gui: failed load %s: %s\n", uiName, err.Error()))
+ }
+
+ return builder
+}
+
+func fileNotFound(fileName string) bool {
+ _, fnf := os.Stat(fileName)
+ return os.IsNotExist(fnf)
+}
+
+func readFile(fileName string) string {
+ file, _ := os.Open(fileName)
+ reader := bufio.NewScanner(file)
+ var content string
+ for reader.Scan() {
+ content = content + reader.Text()
+ }
+ file.Close()
+ return content
+}
+
+func getDefinition(uiName string) fmt.Stringer {
+ def, ok := definitions.Get(uiName)
+ if !ok {
+ panic(fmt.Sprintf("No definition found for %s", uiName))
+ }
+
+ return def
+}
+
+type builder struct {
+ *gtk.Builder
+}
+
+func newBuilder(uiName string) *builder {
+ return &builder{builderForDefinition(uiName)}
+}
+
+func (b *builder) getItem(name string, target interface{}) {
+ v := reflect.ValueOf(target)
+ if v.Kind() != reflect.Ptr {
+ panic("builder.getItem() target argument must be a pointer")
+ }
+ elem := v.Elem()
+ elem.Set(reflect.ValueOf(b.get(name)))
+}
+
+func (b *builder) getItems(args ...interface{}) {
+ for len(args) >= 2 {
+ name, ok := args[0].(string)
+ if !ok {
+ panic("string argument expected in builder.getItems()")
+ }
+ b.getItem(name, args[1])
+ args = args[2:]
+ }
+}
+
+func (b *builder) get(name string) glib.IObject {
+ obj, err := b.GetObject(name)
+ if err != nil {
+ panic("builder.GetObject() failed: " + err.Error())
+ }
+ return obj
+}
diff --git a/fw-settings/config.go b/fw-settings/config.go
new file mode 100644
index 0000000..6aa2542
--- /dev/null
+++ b/fw-settings/config.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+ "github.com/gotk3/gotk3/gtk"
+ "github.com/op/go-logging"
+)
+
+var levelToId = map[int32]string{
+ int32(logging.ERROR): "error",
+ int32(logging.WARNING): "warning",
+ int32(logging.NOTICE): "notice",
+ int32(logging.INFO): "info",
+ int32(logging.DEBUG): "debug",
+}
+
+var idToLevel = func() map[string]int32 {
+ m := make(map[string]int32)
+ for k, v := range levelToId {
+ m[v] = k
+ }
+ return m
+}()
+
+func loadConfig(win *gtk.Window, b *builder, dbus *dbusObject) {
+ var levelCombo *gtk.ComboBoxText
+ var redactCheck *gtk.CheckButton
+
+ b.getItems(
+ "level_combo", &levelCombo,
+ "redact_checkbox", &redactCheck,
+ )
+
+ conf, err := dbus.getConfig()
+ if err != nil {
+ failDialog(win, "Failed to load config from fw daemon: %v", err)
+ }
+
+ if lvl, ok := conf["loglevel"].(int32); ok {
+ if id, ok := levelToId[lvl]; ok {
+ levelCombo.SetActiveID(id)
+ }
+ }
+ if v, ok := conf["logredact"].(bool); ok {
+ redactCheck.SetActive(v)
+ }
+ b.ConnectSignals(map[string]interface{}{
+ "on_level_combo_changed": func() {
+ if lvl, ok := idToLevel[levelCombo.GetActiveID()]; ok {
+ dbus.setConfig("loglevel", lvl)
+ }
+ },
+ "on_redact_checkbox_toggled": func() {
+ dbus.setConfig("logredact", redactCheck.GetActive())
+ },
+ })
+
+}
diff --git a/fw-settings/dbus.go b/fw-settings/dbus.go
new file mode 100644
index 0000000..5010ce7
--- /dev/null
+++ b/fw-settings/dbus.go
@@ -0,0 +1,65 @@
+package main
+
+import (
+ "github.com/godbus/dbus"
+)
+
+type dbusObject struct {
+ dbus.BusObject
+}
+
+type dbusRule struct {
+ Id uint32
+ App string
+ Path string
+ Verb uint32
+ Target string
+}
+
+func newDbusObject() (*dbusObject, error) {
+ conn, err := dbus.SystemBus()
+ if err != nil {
+ return nil, err
+ }
+ return &dbusObject{conn.Object("com.subgraph.Firewall", "/com/subgraph/Firewall")}, nil
+}
+
+func (ob *dbusObject) isEnabled() (bool, error) {
+ var flag bool
+ if err := ob.Call("com.subgraph.Firewall.IsEnabled", 0).Store(&flag); err != nil {
+ return false, err
+ }
+ return flag, nil
+}
+
+func (ob *dbusObject) listRules() ([]dbusRule, error) {
+ rules := []dbusRule{}
+ if err := ob.Call("com.subgraph.Firewall.ListRules", 0).Store(&rules); err != nil {
+ return nil, err
+ }
+ return rules, nil
+}
+
+func (ob *dbusObject) deleteRule(id uint32) {
+ ob.Call("com.subgraph.Firewall.DeleteRule", 0, id)
+}
+
+func (ob *dbusObject) updateRule(rule *dbusRule) {
+ ob.Call("com.subgraph.Firewall.UpdateRule", 0, rule)
+}
+
+func (ob *dbusObject) getConfig() (map[string]interface{}, error) {
+ res := make(map[string]dbus.Variant)
+ if err := ob.Call("com.subgraph.Firewall.GetConfig", 0).Store(&res); err != nil {
+ return nil, err
+ }
+ config := make(map[string]interface{})
+ for k, v := range res {
+ config[k] = v.Value()
+ }
+ return config, nil
+}
+
+func (ob *dbusObject) setConfig(key string, val interface{}) {
+ ob.Call("com.subgraph.Firewall.SetConfig", 0, key, dbus.MakeVariant(val))
+}
diff --git a/fw-settings/definitions/Dialog.xml b/fw-settings/definitions/Dialog.xml
new file mode 100644
index 0000000..867b072
--- /dev/null
+++ b/fw-settings/definitions/Dialog.xml
@@ -0,0 +1,187 @@
+
+
+
+
+
+
diff --git a/fw-settings/definitions/Makefile b/fw-settings/definitions/Makefile
new file mode 100644
index 0000000..ae97f5d
--- /dev/null
+++ b/fw-settings/definitions/Makefile
@@ -0,0 +1,8 @@
+generate:
+ ruby ./generate.rb
+
+touch:
+ ls *.xml | xargs -n1 touch
+
+doctor: touch generate
+ git diff --exit-code .
\ No newline at end of file
diff --git a/fw-settings/definitions/RuleEdit.xml b/fw-settings/definitions/RuleEdit.xml
new file mode 100644
index 0000000..2ad8738
--- /dev/null
+++ b/fw-settings/definitions/RuleEdit.xml
@@ -0,0 +1,201 @@
+
+
+
+
+
+ False
+ Edit Rule
+ dialog
+
+
+ False
+ vertical
+ 2
+
+
+ False
+ end
+
+
+ Cancel
+ True
+ True
+ True
+
+
+ True
+ True
+ 0
+
+
+
+
+ Ok
+ True
+ True
+ True
+ 0.60000002384185791
+
+
+ True
+ True
+ 1
+
+
+
+
+ False
+ False
+ 0
+
+
+
+
+ True
+ False
+ 12
+ 12
+ 12
+ 24
+ 24
+
+
+ True
+ False
+ Enter changes for firewall rule below. The character <b>*</b> can entered alone into either the <b>host</b> or <b>port</b> fields to match any value.
+
+ True
+ True
+ 40
+
+
+ 0
+ 0
+ 2
+
+
+
+
+ True
+ False
+ True
+ 0
+
+
+ 1
+ 1
+
+
+
+
+ True
+ False
+ 12
+ 10
+
+
+ True
+ False
+ 0
+
+ - Allow
+ - Deny
+
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+ Connections to <b>host:</b>
+ True
+
+
+ 1
+ 0
+
+
+
+
+ True
+ True
+ 34
+ hostname or ip address
+
+
+
+ 2
+ 0
+
+
+
+
+ True
+ False
+ on <b>port:</b>
+ True
+
+
+ 3
+ 0
+
+
+
+
+ True
+ True
+ 5
+ 4
+ 5
+ Port
+
+
+
+
+ 4
+ 0
+
+
+
+
+ 0
+ 2
+ 2
+
+
+
+
+ True
+ False
+ 12
+ 10
+ Path:
+ 1
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+ cancel_button
+ ok_button
+
+
+
diff --git a/fw-settings/definitions/RuleItem.xml b/fw-settings/definitions/RuleItem.xml
new file mode 100644
index 0000000..7fdbd75
--- /dev/null
+++ b/fw-settings/definitions/RuleItem.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+ True
+ False
+ True
+
+
+ True
+ False
+ start
+ 8
+ 10
+ 0
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+ 10
+ 1
+
+
+ 1
+ 0
+
+
+
+
+ True
+ False
+ start
+ True
+ 0
+
+
+ 2
+ 0
+
+
+
+
+ True
+ True
+ True
+ none
+
+
+
+ True
+ False
+ document-properties-symbolic
+
+
+
+
+ 3
+ 0
+
+
+
+
+ True
+ True
+ True
+ none
+
+
+
+ True
+ False
+ edit-delete-symbolic
+
+
+
+
+ 4
+ 0
+
+
+
+
diff --git a/fw-settings/definitions/dialog.go b/fw-settings/definitions/dialog.go
new file mode 100644
index 0000000..546f865
--- /dev/null
+++ b/fw-settings/definitions/dialog.go
@@ -0,0 +1,200 @@
+package definitions
+
+func init() {
+ add(`Dialog`, &defDialog{})
+}
+
+type defDialog struct{}
+
+func (*defDialog) String() string {
+ return `
+
+
+
+
+
+ False
+ True
+ Firewall Settings
+ 600
+ 400
+ dialog
+
+
+ True
+ True
+ 5
+
+
+ True
+ False
+ 12
+ 12
+ 12
+ 12
+
+
+ True
+ False
+ Firewall Rules
+ 0
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+ 12
+ 12
+ 8
+ True
+ True
+
+
+ True
+ True
+ True
+ True
+ never
+ in
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 1
+
+
+
+
+ 1
+
+
+
+
+ True
+ False
+ Rules
+
+
+ 1
+ False
+
+
+
+
+ True
+ False
+ 12
+ 12
+
+
+ True
+ False
+ 12
+ 8
+ 6
+ 6
+
+
+ True
+ False
+ start
+ Log Level:
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+ 0
+
+ - Error
+ - Warning
+ - Notice
+ - Info
+ - Debug
+
+
+
+
+ 1
+ 0
+
+
+
+
+ Remove host names and addresses from logs
+ True
+ True
+ False
+ 0
+ True
+
+
+
+ 0
+ 1
+ 2
+
+
+
+
+ 0
+ 1
+
+
+
+
+ True
+ False
+ Logging
+ start
+ 0
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 1
+
+
+
+
+ True
+ False
+ Options
+
+
+ 1
+ False
+
+
+
+
+
+
+
+`
+}
diff --git a/fw-settings/definitions/generate.rb b/fw-settings/definitions/generate.rb
new file mode 100644
index 0000000..12857fe
--- /dev/null
+++ b/fw-settings/definitions/generate.rb
@@ -0,0 +1,47 @@
+#!/usr/bin/env ruby
+
+require 'fileutils'
+
+def parse_go_name(file_name)
+ File.basename(file_name, ".xml").
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
+ tr("-", "_").
+ gsub(/\/_/, '/').
+ downcase + ".go"
+end
+
+def gen_go_file(xml_file, go_file)
+ xml_definition = File.read(xml_file)
+ ui_name = File.basename(xml_file, '.xml')
+ File.open(go_file, 'w+') do |target|
+ target.puts < file_mtime(go_file) || file_mtime(__FILE__) > file_mtime(go_file)
+ STDERR.puts " - #{file_name} -> #{go_file}"
+ gen_go_file(file_name, go_file)
+ end
+end
diff --git a/fw-settings/definitions/management.go b/fw-settings/definitions/management.go
new file mode 100644
index 0000000..2a92dac
--- /dev/null
+++ b/fw-settings/definitions/management.go
@@ -0,0 +1,25 @@
+package definitions
+
+import (
+ "fmt"
+ "sync"
+)
+
+var lock sync.RWMutex
+var definitions = make(map[string]fmt.Stringer)
+
+// Get returns the XML description of a UI definition and whether it was found
+func Get(uiName string) (fmt.Stringer, bool) {
+ lock.RLock()
+ defer lock.RUnlock()
+
+ def, ok := definitions[uiName]
+ return def, ok
+}
+
+func add(uiName string, def fmt.Stringer) {
+ lock.Lock()
+ defer lock.Unlock()
+
+ definitions[uiName] = def
+}
diff --git a/fw-settings/definitions/rule_edit.go b/fw-settings/definitions/rule_edit.go
new file mode 100644
index 0000000..5338086
--- /dev/null
+++ b/fw-settings/definitions/rule_edit.go
@@ -0,0 +1,214 @@
+package definitions
+
+func init() {
+ add(`RuleEdit`, &defRuleEdit{})
+}
+
+type defRuleEdit struct{}
+
+func (*defRuleEdit) String() string {
+ return `
+
+
+
+
+
+ False
+ Edit Rule
+ dialog
+
+
+ False
+ vertical
+ 2
+
+
+ False
+ end
+
+
+ Cancel
+ True
+ True
+ True
+
+
+ True
+ True
+ 0
+
+
+
+
+ Ok
+ True
+ True
+ True
+ 0.60000002384185791
+
+
+ True
+ True
+ 1
+
+
+
+
+ False
+ False
+ 0
+
+
+
+
+ True
+ False
+ 12
+ 12
+ 12
+ 24
+ 24
+
+
+ True
+ False
+ Enter changes for firewall rule below. The character <b>*</b> can entered alone into either the <b>host</b> or <b>port</b> fields to match any value.
+
+ True
+ True
+ 40
+
+
+ 0
+ 0
+ 2
+
+
+
+
+ True
+ False
+ True
+ 0
+
+
+ 1
+ 1
+
+
+
+
+ True
+ False
+ 12
+ 10
+
+
+ True
+ False
+ 0
+
+ - Allow
+ - Deny
+
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+ Connections to <b>host:</b>
+ True
+
+
+ 1
+ 0
+
+
+
+
+ True
+ True
+ 34
+ hostname or ip address
+
+
+
+ 2
+ 0
+
+
+
+
+ True
+ False
+ on <b>port:</b>
+ True
+
+
+ 3
+ 0
+
+
+
+
+ True
+ True
+ 5
+ 4
+ 5
+ Port
+
+
+
+
+ 4
+ 0
+
+
+
+
+ 0
+ 2
+ 2
+
+
+
+
+ True
+ False
+ 12
+ 10
+ Path:
+ 1
+
+
+
+
+
+ 0
+ 1
+
+
+
+
+ False
+ True
+ 1
+
+
+
+
+
+ cancel_button
+ ok_button
+
+
+
+
+`
+}
diff --git a/fw-settings/definitions/rule_item.go b/fw-settings/definitions/rule_item.go
new file mode 100644
index 0000000..991768f
--- /dev/null
+++ b/fw-settings/definitions/rule_item.go
@@ -0,0 +1,101 @@
+package definitions
+
+func init() {
+ add(`RuleItem`, &defRuleItem{})
+}
+
+type defRuleItem struct{}
+
+func (*defRuleItem) String() string {
+ return `
+
+
+
+
+
+ True
+ False
+ True
+
+
+ True
+ False
+ start
+ 10
+ 0
+
+
+ 0
+ 0
+
+
+
+
+ True
+ False
+ 10
+ 1
+
+
+ 1
+ 0
+
+
+
+
+ True
+ False
+ start
+ True
+ 0
+
+
+ 2
+ 0
+
+
+
+
+ True
+ True
+ True
+ none
+
+
+
+ True
+ False
+ document-properties-symbolic
+
+
+
+
+ 3
+ 0
+
+
+
+
+ True
+ True
+ True
+ none
+
+
+
+ True
+ False
+ edit-delete-symbolic
+
+
+
+
+ 4
+ 0
+
+
+
+
+
+`
+}
diff --git a/fw-settings/main.go b/fw-settings/main.go
new file mode 100644
index 0000000..50017eb
--- /dev/null
+++ b/fw-settings/main.go
@@ -0,0 +1,60 @@
+package main
+
+import (
+ "os"
+
+ "fmt"
+ "github.com/gotk3/gotk3/glib"
+ "github.com/gotk3/gotk3/gtk"
+)
+
+func failDialog(parent *gtk.Window, format string, args ...interface{}) {
+ d := gtk.MessageDialogNew(parent, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
+ format, args...)
+ d.Run()
+ os.Exit(1)
+}
+
+func activate(app *gtk.Application) {
+ win := app.GetActiveWindow()
+ if win != nil {
+ win.Present()
+ return
+ }
+
+ var scrolled *gtk.ScrolledWindow
+
+ b := newBuilder("Dialog")
+ b.getItems(
+ "window", &win,
+ "scrolledwindow", &scrolled,
+ )
+ win.SetIconName("security-high-symbolic")
+
+ box, _ := gtk.ListBoxNew()
+ scrolled.Add(box)
+
+ dbus, err := newDbusObject()
+ if err != nil {
+ failDialog(win, "Failed to connect to dbus system bus: %v", err)
+ }
+
+ rl := NewRuleList(dbus, win, box)
+
+ if _, err := dbus.isEnabled(); err != nil {
+ failDialog(win, "Unable is connect to firewall daemon. Is it running?")
+ }
+ rl.loadRules()
+ loadConfig(win, b, dbus)
+ app.AddWindow(win)
+ win.ShowAll()
+}
+
+func main() {
+ app, err := gtk.ApplicationNew("com.subgraph.Firewall.settings", glib.APPLICATION_FLAGS_NONE)
+ if err != nil {
+ panic(fmt.Sprintf("gtk.ApplicationNew() failed: %v", err))
+ }
+ app.Connect("activate", activate)
+ app.Run(os.Args)
+}
diff --git a/fw-settings/rule_edit.go b/fw-settings/rule_edit.go
new file mode 100644
index 0000000..a491c2f
--- /dev/null
+++ b/fw-settings/rule_edit.go
@@ -0,0 +1,161 @@
+package main
+
+import (
+ "fmt"
+ "github.com/gotk3/gotk3/gtk"
+ "net"
+ "strconv"
+ "strings"
+ "unicode"
+)
+
+const (
+ editDialogCancel = 1
+ editDialogOk = 2
+)
+
+type ruleEdit struct {
+ row *ruleRow
+ dialog *gtk.Dialog
+ pathLabel *gtk.Label
+ verbCombo *gtk.ComboBoxText
+ hostEntry *gtk.Entry
+ portEntry *gtk.Entry
+ ok *gtk.Button
+}
+
+func newRuleEdit(rr *ruleRow) *ruleEdit {
+ redit := &ruleEdit{row: rr}
+ b := newBuilder("RuleEdit")
+ b.getItems(
+ "dialog", &redit.dialog,
+ "path_label", &redit.pathLabel,
+ "verb_combo", &redit.verbCombo,
+ "host_entry", &redit.hostEntry,
+ "port_entry", &redit.portEntry,
+ "ok_button", &redit.ok,
+ )
+ b.ConnectSignals(map[string]interface{}{
+ "on_port_insert_text": redit.onPortInsertText,
+ "on_port_changed": redit.onChanged,
+ "on_host_changed": redit.onChanged,
+ })
+ return redit
+}
+
+func (re *ruleEdit) updateDialogFields() {
+ r := re.row.rule
+ re.pathLabel.SetText(r.Path)
+ if r.Verb == RULE_ALLOW {
+ re.verbCombo.SetActiveID("allow")
+ } else {
+ re.verbCombo.SetActiveID("deny")
+ }
+ target := strings.Split(r.Target, ":")
+ if len(target) != 2 {
+ return
+ }
+ re.hostEntry.SetText(target[0])
+ re.portEntry.SetText(target[1])
+}
+
+func (re *ruleEdit) validateFields() bool {
+ id := re.verbCombo.GetActiveID()
+ if id != "allow" && id != "deny" {
+ return false
+ }
+ host, _ := re.hostEntry.GetText()
+ port, _ := re.portEntry.GetText()
+ if !isValidHost(host) {
+ return false
+ }
+ if !isValidPort(port) {
+ return false
+ }
+ return true
+}
+
+func isValidHost(host string) bool {
+ if host == "*" {
+ return true
+ }
+ if net.ParseIP(host) != nil {
+ return true
+ }
+
+ parts := strings.Split(host, ".")
+ if len(parts) < 2 {
+ return false
+ }
+ for _, part := range parts {
+ if part == "" {
+ return false
+ }
+ }
+ return true
+}
+
+func isValidPort(port string) bool {
+ if port == "*" {
+ return true
+ }
+
+ pval, err := strconv.Atoi(port)
+ if err != nil {
+ return false
+ }
+ return pval > 0 && pval <= 0xFFFF
+}
+
+func (re *ruleEdit) updateRow() {
+ if !re.validateFields() {
+ return
+ }
+ r := re.row.rule
+ switch re.verbCombo.GetActiveID() {
+ case "allow":
+ r.Verb = RULE_ALLOW
+ case "deny":
+ r.Verb = RULE_DENY
+ }
+ host, _ := re.hostEntry.GetText()
+ port, _ := re.portEntry.GetText()
+ r.Target = fmt.Sprintf("%s:%s", host, port)
+ re.row.update()
+}
+
+func (re *ruleEdit) run() {
+ re.dialog.SetTransientFor(re.row.rl.win)
+ if re.dialog.Run() == editDialogOk {
+ re.updateRow()
+ re.row.rl.dbus.updateRule(re.row.rule)
+ }
+ re.dialog.Destroy()
+}
+
+func (rr *ruleRow) runEditor() {
+ redit := newRuleEdit(rr)
+ redit.updateDialogFields()
+ redit.run()
+}
+
+func (re *ruleEdit) onPortInsertText(entry *gtk.Entry, text string) {
+ current, _ := entry.GetText()
+ if current == "" && text == "*" {
+ return
+ }
+ if current == "*" {
+ entry.StopEmission("insert-text")
+ return
+ }
+ for _, c := range text {
+ if !unicode.IsDigit(c) {
+ entry.StopEmission("insert-text")
+ return
+ }
+ }
+}
+
+func (re *ruleEdit) onChanged() {
+ re.ok.SetSensitive(re.validateFields())
+}
diff --git a/fw-settings/rules.go b/fw-settings/rules.go
new file mode 100644
index 0000000..341c648
--- /dev/null
+++ b/fw-settings/rules.go
@@ -0,0 +1,147 @@
+package main
+
+import (
+ "fmt"
+ "github.com/gotk3/gotk3/gtk"
+ "strings"
+)
+
+type ruleList struct {
+ dbus *dbusObject
+ win *gtk.Window
+ list *gtk.ListBox
+ col1 *gtk.SizeGroup
+ col2 *gtk.SizeGroup
+ col3 *gtk.SizeGroup
+}
+
+type ruleRow struct {
+ rl *ruleList
+ rule *dbusRule
+ widget *gtk.ListBoxRow
+ app_label *gtk.Label
+ verb_label *gtk.Label
+ target_label *gtk.Label
+}
+
+func NewRuleList(dbus *dbusObject, win *gtk.Window, list *gtk.ListBox) *ruleList {
+ rl := &ruleList{dbus: dbus, win: win, list: list}
+ rl.col1, _ = gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL)
+ rl.col2, _ = gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL)
+ rl.col3, _ = gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL)
+ return rl
+}
+
+func (rl *ruleList) loadRules() error {
+ rules, err := rl.dbus.listRules()
+ if err != nil {
+ return err
+ }
+ rl.addRules(rules)
+ return nil
+}
+
+func (rl *ruleList) addRules(rules []dbusRule) {
+ for i := 0; i < len(rules); i++ {
+ row := createWidget(&rules[i])
+ row.rl = rl
+ rl.col1.AddWidget(row.app_label)
+ rl.col2.AddWidget(row.verb_label)
+ rl.col3.AddWidget(row.target_label)
+ rl.list.Add(row.widget)
+ }
+}
+
+const RULE_DENY = 0
+const RULE_ALLOW = 1
+
+func createWidget(rule *dbusRule) *ruleRow {
+ row := &ruleRow{}
+ row.rule = rule
+ builder := newBuilder("RuleItem")
+ var grid *gtk.Grid
+ builder.getItems(
+ "grid", &grid,
+ "app_label", &row.app_label,
+ "verb_label", &row.verb_label,
+ "target_label", &row.target_label,
+ )
+ builder.ConnectSignals(map[string]interface{}{
+ "on_edit_rule": row.onEdit,
+ "on_delete_rule": row.onDelete,
+ })
+ row.widget, _ = gtk.ListBoxRowNew()
+ row.widget.Add(grid)
+ row.update()
+ return row
+}
+
+func (rr *ruleRow) update() {
+ rr.app_label.SetText(rr.rule.App)
+ rr.app_label.SetTooltipText(rr.rule.Path)
+ rr.verb_label.SetText(getVerbText(rr.rule))
+ rr.target_label.SetText(getTargetText(rr.rule))
+}
+
+func getVerbText(rule *dbusRule) string {
+ if rule.Verb == RULE_ALLOW {
+ return "ALLOW:"
+ }
+ return "DENY:"
+}
+
+func getTargetText(rule *dbusRule) string {
+ if rule.Target == "*:*" {
+ return "All connections"
+ }
+ items := strings.Split(rule.Target, ":")
+
+ if len(items) != 2 {
+ return rule.Target
+ }
+
+ if items[0] == "*" {
+ return fmt.Sprintf("Connections to All hosts on port %s", items[1])
+ }
+ if items[1] == "*" {
+ return fmt.Sprintf("All connections to host %s")
+ }
+
+ return fmt.Sprintf("Connections to %s on port %s", items[0], items[1])
+}
+
+func (rr *ruleRow) onEdit() {
+ rr.runEditor()
+}
+
+func (rr *ruleRow) onDelete() {
+ body := fmt.Sprintf(`Are you sure you want to delete this rule:
+
+ Path: %s
+
+ Rule: %s %s`, rr.rule.Path, getVerbText(rr.rule), getTargetText(rr.rule))
+ d := gtk.MessageDialogNewWithMarkup(
+ rr.rl.win,
+ gtk.DIALOG_DESTROY_WITH_PARENT,
+ gtk.MESSAGE_QUESTION,
+ gtk.BUTTONS_OK_CANCEL,
+ "")
+ d.SetMarkup(body)
+ if d.Run() == (int)(gtk.RESPONSE_OK) {
+ rr.delete()
+ }
+ d.Destroy()
+
+}
+
+func (rl *ruleList) remove(rr *ruleRow) {
+ rl.col1.RemoveWidget(rr.app_label)
+ rl.col2.RemoveWidget(rr.verb_label)
+ rl.col3.RemoveWidget(rr.target_label)
+ rl.list.Remove(rr.widget)
+}
+
+func (rr *ruleRow) delete() {
+ rr.rl.remove(rr)
+ rr.rl.dbus.deleteRule(rr.rule.Id)
+}