diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 6e211d3..404d0a6 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,7 +1,7 @@ { "ImportPath": "github.com/subgraph/fw-daemon", "GoVersion": "go1.7", - "GodepVersion": "v79", + "GodepVersion": "v74", "Deps": [ { "ImportPath": "github.com/godbus/dbus", @@ -45,15 +45,15 @@ }, { "ImportPath": "github.com/subgraph/go-nfnetlink", - "Rev": "34abd96bd88d0fbc1a5bbba9fff1bdc2c0448f47" + "Rev": "bd5c281b400452a89af0cf298503f5d0bddc57c2" }, { "ImportPath": "github.com/subgraph/go-nfnetlink/nfqueue", - "Rev": "34abd96bd88d0fbc1a5bbba9fff1bdc2c0448f47" + "Rev": "bd5c281b400452a89af0cf298503f5d0bddc57c2" }, { "ImportPath": "github.com/subgraph/go-procsnitch", - "Rev": "fbc2965632eec2dcea9b8d630b081b10980d325d" + "Rev": "26d0071b72fb28493634fff6b2194db40114f28a" }, { "ImportPath": "github.com/subgraph/oz/ipc", @@ -63,6 +63,11 @@ { "ImportPath": "github.com/subgraph/ozipc", "Rev": "2cbf2ba8878e9dea012edb021f3b4836f7c93202" + }, + { + "ImportPath": "github.com/subgraph/oz/vendor/github.com/op/go-logging", + "Comment": "0.2.5", + "Rev": "69e556d2d33a76f19546471d813a21f7b7fa4181" } ] } diff --git a/fw-settings/builder.go b/fw-settings/builder.go index b19593f..f980924 100644 --- a/fw-settings/builder.go +++ b/fw-settings/builder.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "encoding/xml" "fmt" "os" "path/filepath" @@ -18,6 +19,51 @@ const ( xmlExtension = ".xml" ) +type GtkXMLInterface struct { + XMLName xml.Name `xml:"interface"` + Objects []*GtkXMLObject `xml:"object"` + Requires *GtkXMLRequires `xml:"requires"` + Comment string `xml:",comment"` +} + +type GtkXMLRequires struct { + Lib string `xml:"lib,attr"` + Version string `xml:"version,attr"` +} + +type GtkXMLObject struct { + XMLName xml.Name `xml:"object"` + Class string `xml:"class,attr"` + ID string `xml:"id,attr,omitempty"` + Properties []GtkXMLProperty `xml:"property"` + Children []GtkXMLChild `xml:"child,omitempty"` + Signals []GtkXMLSignal `xml:"signal,omitempty"` +} + +type GtkXMLChild struct { + XMLName xml.Name `xml:"child"` + Objects []*GtkXMLObject `xml:"object"` + Placeholder *GtkXMLPlaceholder `xml:"placeholder,omitempty"` + InternalChild string `xml:"internal-child,attr,omitempty"` +} + +type GtkXMLProperty struct { + XMLName xml.Name `xml:"property"` + Name string `xml:"name,attr"` + Translatable string `xml:"translatable,attr,omitempty"` + Value string `xml:",chardata"` +} + +type GtkXMLSignal struct { + XMLName xml.Name `xml:"signal"` + Name string `xml:"name,attr"` + Handler string `xml:"handler,attr"` +} + +type GtkXMLPlaceholder struct { + XMLName xml.Name `xml:"placeholder"` +} + func getDefinitionWithFileFallback(uiName string) string { // this makes sure a missing definition wont break only when the app is released uiDef := getDefinition(uiName) @@ -30,6 +76,38 @@ func getDefinitionWithFileFallback(uiName string) string { return readFile(fileName) } +// This must be called from the UI thread - otherwise bad things will happen sooner or later +func builderForString(template string) *gtk.Builder { + // assertInUIThread() + + maj := gtk.GetMajorVersion() + min := gtk.GetMinorVersion() + + if (maj == 3) && (min < 20) { + fmt.Fprintf(os.Stderr, + "Attempting runtime work-around for older versions of libgtk-3...\n") + dep_re := regexp.MustCompile(`<\s?property\s+name\s?=\s?"icon_size"\s?>.+<\s?/property\s?>`) + template = dep_re.ReplaceAllString(template, ``) + + dep_re2 := regexp.MustCompile(`version\s?=\s?"3.20"`) + template = dep_re2.ReplaceAllString(template, `version="3.18"`) + } + + 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 string template: %s\n", err.Error())) + } + + return builder +} + // This must be called from the UI thread - otherwise bad things will happen sooner or later func builderForDefinition(uiName string) *gtk.Builder { // assertInUIThread() @@ -97,6 +175,10 @@ func newBuilder(uiName string) *builder { return &builder{builderForDefinition(uiName)} } +func newBuilderFromString(template string) *builder { + return &builder{builderForString(template)} +} + func (b *builder) getItem(name string, target interface{}) { v := reflect.ValueOf(target) if v.Kind() != reflect.Ptr { diff --git a/fw-settings/config.go b/fw-settings/config.go index 15be978..d2e820d 100644 --- a/fw-settings/config.go +++ b/fw-settings/config.go @@ -1,29 +1,37 @@ package main import ( - "github.com/gotk3/gotk3/gtk" - "github.com/subgraph/fw-daemon/sgfw" + + "github.com/gotk3/gotk3/glib" + "github.com/gotk3/gotk3/gtk" ) -func loadConfig(win *gtk.Window, b *builder, dbus *dbusObject) { +type cbConfigChanged func() +var configCallbacks []cbConfigChanged + +func (fa *fwApp) loadConfig(init bool) { var levelCombo *gtk.ComboBoxText var redactCheck *gtk.CheckButton var expandedCheck *gtk.CheckButton var expertCheck *gtk.CheckButton var actionCombo *gtk.ComboBoxText + var gridPrompt *gtk.Grid + var toplevelCheck *gtk.CheckButton - b.getItems( + fa.winb.getItems( "level_combo", &levelCombo, "redact_checkbox", &redactCheck, "expanded_checkbox", &expandedCheck, "expert_checkbox", &expertCheck, "action_combo", &actionCombo, + "grib_gtkprompt", &gridPrompt, + "toplevel_checkbox", &toplevelCheck, ) - conf, err := dbus.getConfig() + conf, err := fa.Dbus.getConfig() if err != nil { - failDialog(win, "Failed to load config from fw daemon: %v", err) + failDialog(&fa.win.Window, "Failed to load config from fw daemon: %v", err) } if lvl, ok := conf["log_level"].(int32); ok { @@ -35,32 +43,114 @@ func loadConfig(win *gtk.Window, b *builder, dbus *dbusObject) { redactCheck.SetActive(v) } if v, ok := conf["prompt_expanded"].(bool); ok { + fa.Config.PromptExpanded = v expandedCheck.SetActive(v) } if v, ok := conf["prompt_expert"].(bool); ok { + fa.Config.PromptExpert = v expertCheck.SetActive(v) } if av, ok := conf["default_action"].(uint16); ok { - actionCombo.SetActiveID(sgfw.GetFilterScopeString(sgfw.FilterScope(av))) + v := sgfw.GetFilterScopeString(sgfw.FilterScope(av)) + fa.Config.DefaultAction = v + actionCombo.SetActiveID(v) } - b.ConnectSignals(map[string]interface{}{ - "on_level_combo_changed": func() { + + if fa.promptMode == promptModeDisabled { + gridPrompt.SetNoShowAll(true) + gridPrompt.SetVisible(false) + expertCheck.SetTooltipText("") + } else { + l := expertCheck.GetChildren() + ect, _ := expertCheck.GetLabel() + ecl := gtk.Label{*l.NthData(0).(*gtk.Widget)} + ecl.SetUseMarkup(true) + ecl.SetMarkup("" + ect + "") + expertCheck.SetTooltipText("Applies only when using the GNOME Shell Prompter") + + gridPrompt.SetNoShowAll(false) + gridPrompt.SetVisible(true) + toplevelCheck.SetActive(fa.Settings.GetToplevelPrompt()) + } + + if init { + levelCombo.Connect("changed", func() { if lvl, ok := sgfw.IDToLevel[levelCombo.GetActiveID()]; ok { - dbus.setConfig("log_level", lvl) + fa.Dbus.setConfig("log_level", lvl) + } + }) + var redactHandler glib.SignalHandle + redactHandler, _ = redactCheck.Connect("toggled", func() { + val := redactCheck.GetActive() + if val { + fa.Dbus.setConfig("log_redact", val) + return } - }, - "on_redact_checkbox_toggled": func() { - dbus.setConfig("log_redact", redactCheck.GetActive()) - }, - "on_expanded_checkbox_toggled": func() { - dbus.setConfig("prompt_expanded", expandedCheck.GetActive()) - }, - "on_expert_checkbox_toggled": func() { - dbus.setConfig("prompt_expert", expertCheck.GetActive()) - }, - "on_action_combo_changed": func() { - dbus.setConfig("default_action", sgfw.GetFilterScopeValue(actionCombo.GetActiveID())) - }, - }) + if fa.promptWarnLogRedact() { + fa.Dbus.setConfig("log_redact", val) + } else { + redactCheck.HandlerBlock(redactHandler) + redactCheck.SetActive(true) + redactCheck.HandlerUnblock(redactHandler) + } + }) + expandedCheck.Connect("toggled", func() { + v := expandedCheck.GetActive() + fa.Config.PromptExpanded = v + fa.Dbus.setConfig("prompt_expanded", v) + fa.triggerConfigCallbacks() + }) + expertCheck.Connect("toggled", func() { + v := expertCheck.GetActive() + fa.Config.PromptExpert = v + fa.Dbus.setConfig("prompt_expert", v) + fa.triggerConfigCallbacks() + }) + actionCombo.Connect("changed", func() { + v := sgfw.GetFilterScopeValue(actionCombo.GetActiveID()) + fa.Config.DefaultAction = string(sgfw.FilterScope(v)) + fa.Dbus.setConfig("default_action", v) + fa.triggerConfigCallbacks() + }) + if fa.promptMode != promptModeDisabled { + toplevelCheck.Connect("toggled", func() { + fa.Settings.SetToplevelPrompt(toplevelCheck.GetActive()) + }) + } + } +} + +func (fa *fwApp) promptWarnLogRedact() bool { + res := false + body := "Are you sure you want to unredact logs?" + msg := "Sensitive information may get saved to the disk!" + d := gtk.MessageDialogNewWithMarkup( + fa.win, + gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION, + gtk.BUTTONS_OK_CANCEL, + "") + d.SetMarkup(body) + d.SetProperty("secondary-text", msg) + if d.Run() == (int)(gtk.RESPONSE_OK) { + res = true + } else { + fa.win.SetUrgencyHint(false) + fa.win.SetKeepAbove(false) + } + d.Destroy() + return res +} +func (fa *fwApp) appendConfigCallback(fn cbConfigChanged) { + configCallbacks = append(configCallbacks, fn) +} + +func (fa *fwApp) triggerConfigCallbacks() { + for _, fn := range configCallbacks { + glib.IdleAdd(func () bool { + fn() + return false + }) + } } diff --git a/fw-settings/dbus.go b/fw-settings/dbus.go index 1e877b1..fe8f3fa 100644 --- a/fw-settings/dbus.go +++ b/fw-settings/dbus.go @@ -3,19 +3,21 @@ package main import ( "errors" "fmt" + "os" + "strings" + + "github.com/subgraph/fw-daemon/sgfw" + "github.com/godbus/dbus" + "github.com/godbus/dbus/introspect" + "github.com/gotk3/gotk3/glib" - "github.com/subgraph/fw-daemon/sgfw" ) type dbusObject struct { dbus.BusObject } -type dbusObjectP struct { - dbus.BusObject -} - type dbusServer struct { conn *dbus.Conn run bool @@ -29,14 +31,6 @@ func newDbusObject() (*dbusObject, error) { return &dbusObject{conn.Object("com.subgraph.Firewall", "/com/subgraph/Firewall")}, nil } -func newDbusObjectPrompt() (*dbusObjectP, error) { - conn, err := dbus.SystemBus() - if err != nil { - return nil, err - } - return &dbusObjectP{conn.Object("com.subgraph.fwprompt.EventNotifier", "/com/subgraph/fwprompt/EventNotifier")}, nil -} - func (ob *dbusObject) isEnabled() (bool, error) { var flag bool if err := ob.Call("com.subgraph.Firewall.IsEnabled", 0).Store(&flag); err != nil { @@ -55,11 +49,45 @@ func (ob *dbusObject) listRules() ([]sgfw.DbusRule, error) { } func (ob *dbusObject) deleteRule(id uint32) { - ob.Call("com.subgraph.Firewall.DeleteRule", 0, id) + fmt.Printf("Deleting rule: %d\n", id) + res := ob.Call("com.subgraph.Firewall.DeleteRule", 0, id) + if res.Err != nil { + fmt.Printf("DBUS Delete error with %+v\n", res.Err) + } } func (ob *dbusObject) updateRule(rule *sgfw.DbusRule) { - ob.Call("com.subgraph.Firewall.UpdateRule", 0, rule) + fmt.Printf("Updating rule: %+v\n", rule) + res := ob.Call("com.subgraph.Firewall.UpdateRule", 0, rule) + if res.Err != nil { + fmt.Printf("DBUS UPdate error with %+v\n", res.Err) + } +} + +func (ob *dbusObject) answerPrompt(scope uint32, rule, guid string) error { + var dres bool + call := ob.Call("AddRuleAsync", 0, uint32(scope), rule, "*", guid) + + err := call.Store(&dres) + if err != nil { + fmt.Printf("Error notifying SGFW of asynchronous rule addition: %+v\n", err) + return err + } + + fmt.Println("makeDecision remote result:", dres) + return nil +} + +func (ob *dbusObject) addRule(rule *sgfw.DbusRule) (bool, error) { + var dres bool + fmt.Printf("Adding new rule: %+v\n", rule) + call := ob.Call("com.subgraph.Firewall.AddNewRule", 0, rule) + err := call.Store(&dres) + if err != nil { + fmt.Println("Error while adding new rule:", err) + return false, err + } + return dres, nil } func (ob *dbusObject) getConfig() (map[string]interface{}, error) { @@ -78,14 +106,109 @@ func (ob *dbusObject) setConfig(key string, val interface{}) { ob.Call("com.subgraph.Firewall.SetConfig", 0, key, dbus.MakeVariant(val)) } -func newDbusServer() (*dbusServer, error) { +func dbusSignalHandler(app *fwApp) { + for { + conn, err := dbus.SystemBus() + if err != nil { + fmt.Fprintln(os.Stderr, "Failed to connect to bus: ", err) + } + defer conn.Close() + + conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, + "type='signal',path='/com/subgraph/Firewall',interface='com.subgraph.Firewall',sender='com.subgraph.Firewall'") + + c := make(chan *dbus.Signal, 10) + conn.Signal(c) + for v := range c { + if !strings.HasPrefix(v.Name, "com.subgraph.Firewall.") { + continue + } + if len(v.Body) == 0 { + continue + } + val := v.Body[0].(string) + name := strings.ToLower(strings.Replace(v.Name, "com.subgraph.Firewall.", "", 1)) + fmt.Printf("Received Dbus update alert: %s(%v)\n", name, val) + switch name { + case "refresh": + switch val { + case "init": + glib.IdleAdd(func () bool { + if app.promptMode != promptModeDisabled { + app.prompt.RemoveAll() + } + app.handleRefreshRules() + app.handleRefreshConfig() + return false + }) + case "rules": + glib.IdleAdd(func () bool { + app.handleRefreshRules() + return false + }) + case "config": + glib.IdleAdd(func () bool { + app.handleRefreshConfig() + return false + }) + default: + continue + } + default: + continue + } + } + } +} + + +/* + * DBus Prompt Service + */ + +const introspectPromptXML = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` + + introspect.IntrospectDataString + + `` + +func newPromptDbusServer() (*dbusServer, error) { conn, err := dbus.SystemBus() if err != nil { return nil, err } - reply, err := conn.RequestName("com.subgraph.fwprompt.EventNotifier", dbus.NameFlagDoNotQueue) + reply, err := conn.RequestName("com.subgraph.FirewallPrompt", dbus.NameFlagDoNotQueue) if err != nil { return nil, err @@ -97,21 +220,39 @@ func newDbusServer() (*dbusServer, error) { ds := &dbusServer{} - if err := conn.Export(ds, "/com/subgraph/fwprompt/EventNotifier", "com.subgraph.fwprompt.EventNotifier"); err != nil { + if err := conn.Export(ds, "/com/subgraph/FirewallPrompt", "com.subgraph.FirewallPrompt"); err != nil { + return nil, err + } + + if err := conn.Export(introspect.Introspectable(introspectPromptXML), "/com/subgraph/FirewallPrompt", "org.freedesktop.DBus.Introspectable"); err != nil { return nil, err } ds.conn = conn ds.run = true + return ds, nil } -func (ds *dbusServer) Alert(data string) *dbus.Error { - fmt.Println("Received Dbus update alert: ", data) - glib.IdleAdd(repopulateWin) - return nil +func (ds *dbusServer) RequestPromptAsync(guid, application, icon, path, address string, port int32, ip, origin, proto string, uid, gid int32, username, groupname string, pid int32, sandbox string, + is_socks bool, timestamp string, optstring string, expanded, expert bool, action int32) (bool, *dbus.Error) { + fmt.Printf("ASYNC request prompt: guid = %s, app = %s, icon = %s, path = %s, address = %s / ip = %s, is_socks = %v, sandbox = %v, action = %v\n", guid, application, icon, path, address, ip, is_socks, sandbox, action) + if cbPromptAddRequest != nil { + glib.IdleAdd(func () bool { + cbPromptAddRequest(guid, path, icon, proto, int(pid), ip, address, int(port), int(uid), int(gid), origin, timestamp, is_socks, optstring, sandbox, int(action)) + return false + }) + } + return true, nil } -func (ob *dbusObjectP) alertRule(data string) { - ob.Call("com.subgraph.fwprompt.EventNotifier.Alert", 0, data) +func (ds *dbusServer) RemovePrompt(guid string) *dbus.Error { + fmt.Printf("++++++++ Cancelling prompt: %s\n", guid) + if cbPromptRemoveRequest != nil { + glib.IdleAdd(func () bool { + cbPromptRemoveRequest(guid) + return false + }) + } + return nil } diff --git a/fw-settings/definitions/Dialog.ui b/fw-settings/definitions/Dialog.ui index f71815e..87f9477 100644 --- a/fw-settings/definitions/Dialog.ui +++ b/fw-settings/definitions/Dialog.ui @@ -2,12 +2,22 @@ - + + True + False + document-new-symbolic + + + True + False + edit-find-symbolic + + False Subgraph Firewall center - 600 - 400 + 700 + 600 False @@ -18,123 +28,218 @@ False 5 - 1000 + 250 + slide-left-right - + True True - True - True + in - - True - True - True - True - never - in - - - True - - - - - True - False - Permanent - - - True - False - - - - - True - True - True - True - never - in - - - 1 - True - - - - - True - False - Session - - - 1 - True - False - + + + + prompt + Prompt + + + + + True + False + vertical - - True - True - True - True - never - in - - - 2 - True - - - - + True False - Process + slide-up + + + True + True + Rule path or sandbox + center + 5 + 5 + 48 + True + False + edit-find-symbolic + False + False + Rule path or sandbox + + - 2 - True - False + False + True + 0 - + True True True True - never - in - - - 3 - True - - - - - True - False - System + + + True + True + True + True + never + in + + + True + + + + + True + False + Permanent + + + True + False + + + + + True + True + True + True + never + in + + + 1 + True + + + + + True + False + Session + + + 1 + True + False + + + + + True + True + True + True + never + in + + + 2 + True + + + + + True + False + Process + + + 2 + True + False + + + + + True + True + True + True + never + in + + + 3 + True + + + + + True + False + System + + + 3 + True + False + + + + + True + False + 5 + 5 + 2 + 2 + + + True + True + True + img_btn_search + + + False + True + 0 + + + + + True + False + 5 + + + False + True + 1 + + + + + False + + - 3 - True - False + False + True + 1 - page0 + rules Rules + 1 - + True False 10 @@ -208,7 +313,6 @@ Info Debug - 1 @@ -224,7 +328,6 @@ start 10 True - 0 @@ -241,7 +344,6 @@ start 10 True - 0 @@ -258,7 +360,6 @@ start 10 True - 0 @@ -276,7 +377,7 @@ 0 - 6 + 7 @@ -288,20 +389,49 @@ Forever Session + Process Once - 1 + 7 + + + + + True + False + 5 + True + + + Bring window front most and make sticky on new prompt events + True + True + False + start + 10 + True + + + 0 + 0 + 2 + + + + + 0 6 + 2 - page1 + config Options - 1 + 2 @@ -318,12 +448,83 @@ False True :minimize,maximize,close + + + False + True + center + + + True + True + True + + + False + open-menu-symbolic + 1 + + + + + + False + True + 0 + + + + + True + False + 5 + vertical + + + False + True + 1 + + + + + + + True + True + True + True + True + False + Add New Rule + center + False + False + img_btn_add + + + + + 1 + + False + center 2 toplevel_stack + + 2 + diff --git a/fw-settings/definitions/RuleItem.ui b/fw-settings/definitions/RuleItem.ui index afe2ff6..eb953e9 100644 --- a/fw-settings/definitions/RuleItem.ui +++ b/fw-settings/definitions/RuleItem.ui @@ -6,13 +6,12 @@ True False True + True - + True False - start - 8 - 10 + gtk-missing-image 0 @@ -20,11 +19,13 @@ - + True False start + 8 10 + 0 1 @@ -32,39 +33,28 @@ - + True False - start - 10 + gtk-no 2 0 - - - True - False - start - 10 - - - 3 - 0 - - - True False start + 10 True + 10 + 0 - 4 + 3 0 @@ -85,45 +75,45 @@ - 5 + 4 0 - - True + + False True True - Delete firewall rule + True + Save as new permanent firewall rule none - + - + True False - edit-delete-symbolic + document-new-symbolic - 7 + 5 0 - - False + + True True True - True - Save as new permanent firewall rule + Delete firewall rule none - + - + True False - document-new-symbolic + edit-delete-symbolic diff --git a/fw-settings/definitions/RuleNew.ui b/fw-settings/definitions/RuleNew.ui new file mode 100644 index 0000000..43dad84 --- /dev/null +++ b/fw-settings/definitions/RuleNew.ui @@ -0,0 +1,573 @@ + + + + + + False + Edit Rule + SubgraphFirewallNewRule + False + True + center + True + alacarte + dialog + True + False + SubgraphFirewallNewRule + + + False + center + center + 10 + 10 + 10 + 10 + vertical + 5 + + + False + True + True + spread + + + _Allow + False + True + True + True + True + + + True + True + 0 + + + + + _Cancel + True + True + True + True + True + + + True + True + 1 + + + + + _Ok + True + True + True + True + True + + + True + True + 2 + + + + + False + False + 3 + + + + + True + False + 0 + + Allow + Deny + + + + + False + True + 0 + + + + + True + False + 5 + 10 + True + + + True + False + start + False + Path: + + + + + + 0 + 0 + + + + + True + False + start + False + Sandbox: + + + + + + 0 + 1 + + + + + True + False + start + False + UID: + + + + + + 0 + 7 + + + + + True + False + start + False + GID: + + + + + + 0 + 8 + + + + + True + False + False + False + False + Select Executable Path + + + + + 1 + 0 + + + + + True + False + start + False + Scope: + + + + + + 0 + 9 + + + + + True + False + + + 1 + 1 + + + + + True + False + start + False + Protocol: + + + + + + 0 + 6 + + + + + True + False + The character <b>*</b> can be use to match any value. + start + False + Host: + + + + + + 0 + 4 + + + + + True + True + The character <b>*</b> can be use to match any value. + 64 + 34 + The character <b>*</b> can be use to match any value. + Hostname or IP address + + + + 1 + 4 + + + + + True + True + The character <b>*</b> can be use to match any value. + 5 + 4 + 5 + The character <b>*</b> can be use to match any value. + Port + + + + + 1 + 5 + + + + + True + False + The character <b>*</b> can be use to match any value. + start + False + Port: + + + + + + 0 + 5 + + + + + False + True + start + False + SANDBOX + + + 1 + 1 + + + + + False + True + start + False + Pid: + + + + + + 0 + 2 + + + + + False + True + start + False + PID_LABEL + + + 1 + 2 + + + + + False + True + start + False + SCOPE_LABEL + + + 1 + 9 + + + + + True + False + True + top + + + True + False + 0 + -1 + + Any User + + + + False + True + 0 + + + + + Apply + False + True + False + True + 0 + True + + + False + True + 1 + + + + + 1 + 7 + + + + + True + False + True + + + True + False + 0 + -1 + + Any Group + + + + False + True + 0 + + + + + Apply + False + True + False + True + 0 + True + + + False + True + 1 + + + + + 1 + 8 + + + + + False + True + True + + + 1 + 0 + + + + + False + True + The character <b>*</b> can be use to match any value. + start + False + Origin: + + + + + + 0 + 3 + + + + + False + True + True + + + 1 + 3 + + + + + True + False + 0 + 0 + + Permanent + Session + Process + Once + System + + + + 1 + 9 + + + + + True + False + True + + + True + False + 1 + 1 + + Any + TCP + UDP + ICMP + + + + + False + True + 0 + + + + + TLS Only + True + True + False + True + + + False + True + 1 + + + + + 1 + 6 + + + + + False + True + 5 + 1 + + + + + True + False + + + False + True + 2 + + + + + + allow_button + cancel_button + ok_button + + + diff --git a/fw-settings/definitions/dialog.go b/fw-settings/definitions/dialog.go index 85a1f25..fb0b78c 100644 --- a/fw-settings/definitions/dialog.go +++ b/fw-settings/definitions/dialog.go @@ -12,12 +12,22 @@ func (*defDialog) String() string { - + + True + False + document-new-symbolic + + + True + False + edit-find-symbolic + + False Subgraph Firewall center - 600 - 400 + 700 + 600 False @@ -28,123 +38,218 @@ func (*defDialog) String() string { False 5 - 1000 + 250 + slide-left-right - + True True - True - True + in - - True - True - True - True - never - in - - - True - - - - - True - False - Permanent - - - True - False - - - - - True - True - True - True - never - in - - - 1 - True - - - - - True - False - Session - - - 1 - True - False - + + + + prompt + Prompt + + + + + True + False + vertical - - True - True - True - True - never - in - - - 2 - True - - - - + True False - Process + slide-up + + + True + True + Rule path or sandbox + center + 5 + 5 + 48 + True + False + edit-find-symbolic + False + False + Rule path or sandbox + + - 2 - True - False + False + True + 0 - + True True True True - never - in - - - 3 - True - - - - - True - False - System + + + True + True + True + True + never + in + + + True + + + + + True + False + Permanent + + + True + False + + + + + True + True + True + True + never + in + + + 1 + True + + + + + True + False + Session + + + 1 + True + False + + + + + True + True + True + True + never + in + + + 2 + True + + + + + True + False + Process + + + 2 + True + False + + + + + True + True + True + True + never + in + + + 3 + True + + + + + True + False + System + + + 3 + True + False + + + + + True + False + 5 + 5 + 2 + 2 + + + True + True + True + img_btn_search + + + False + True + 0 + + + + + True + False + 5 + + + False + True + 1 + + + + + False + + - 3 - True - False + False + True + 1 - page0 + rules Rules + 1 - + True False 10 @@ -218,7 +323,6 @@ func (*defDialog) String() string { Info Debug - 1 @@ -234,7 +338,6 @@ func (*defDialog) String() string { start 10 True - 0 @@ -251,7 +354,6 @@ func (*defDialog) String() string { start 10 True - 0 @@ -268,7 +370,6 @@ func (*defDialog) String() string { start 10 True - 0 @@ -286,7 +387,7 @@ func (*defDialog) String() string { 0 - 6 + 7 @@ -301,18 +402,46 @@ func (*defDialog) String() string { Process Once - 1 + 7 + + + + + True + False + 5 + True + + + Bring window front most and make sticky on new prompt events + True + True + False + start + 10 + True + + + 0 + 0 + 2 + + + + + 0 6 + 2 - page1 + config Options - 1 + 2 @@ -329,12 +458,83 @@ func (*defDialog) String() string { False True :minimize,maximize,close + + + False + True + center + + + True + True + True + + + False + open-menu-symbolic + 1 + + + + + + False + True + 0 + + + + + True + False + 5 + vertical + + + False + True + 1 + + + + + + + True + True + True + True + True + False + Add New Rule + center + False + False + img_btn_add + + + + + 1 + + False + center 2 toplevel_stack + + 2 + diff --git a/fw-settings/definitions/rule_item.go b/fw-settings/definitions/rule_item.go index 648294b..dcbc5c8 100644 --- a/fw-settings/definitions/rule_item.go +++ b/fw-settings/definitions/rule_item.go @@ -16,13 +16,12 @@ func (*defRuleItem) String() string { True False True + True - + True False - start - 8 - 10 + gtk-missing-image 0 @@ -30,11 +29,13 @@ func (*defRuleItem) String() string { - + True False start + 8 10 + 0 1 @@ -42,38 +43,28 @@ func (*defRuleItem) String() string { - + True False - start - 10 + gtk-no 2 0 - - - True - False - start - 10 - - - 3 - 0 - - True False start + 10 True + 10 + 0 - 4 + 3 0 @@ -94,45 +85,45 @@ func (*defRuleItem) String() string { - 5 + 4 0 - - True + + False True True - Delete firewall rule + True + Save as new permanent firewall rule none - + - + True False - edit-delete-symbolic + document-new-symbolic - 7 + 5 0 - - False + + True True True - True - Save as new permanent firewall rule + Delete firewall rule none - + - + True False - document-new-symbolic + edit-delete-symbolic diff --git a/fw-settings/definitions/rule_new.go b/fw-settings/definitions/rule_new.go new file mode 100644 index 0000000..7b33a5b --- /dev/null +++ b/fw-settings/definitions/rule_new.go @@ -0,0 +1,586 @@ +package definitions + +func init() { + add(`RuleNew`, &defRuleNew{}) +} + +type defRuleNew struct{} + +func (*defRuleNew) String() string { + return ` + + + + + + False + Edit Rule + SubgraphFirewallNewRule + False + True + center + True + alacarte + dialog + True + False + SubgraphFirewallNewRule + + + False + center + center + 10 + 10 + 10 + 10 + vertical + 5 + + + False + True + True + spread + + + _Allow + False + True + True + True + True + + + True + True + 0 + + + + + _Cancel + True + True + True + True + True + + + True + True + 1 + + + + + _Ok + True + True + True + True + True + + + True + True + 2 + + + + + False + False + 3 + + + + + True + False + 0 + + Allow + Deny + + + + + False + True + 0 + + + + + True + False + 5 + 10 + True + + + True + False + start + False + Path: + + + + + + 0 + 0 + + + + + True + False + start + False + Sandbox: + + + + + + 0 + 1 + + + + + True + False + start + False + UID: + + + + + + 0 + 7 + + + + + True + False + start + False + GID: + + + + + + 0 + 8 + + + + + True + False + False + False + False + Select Executable Path + + + + + 1 + 0 + + + + + True + False + start + False + Scope: + + + + + + 0 + 9 + + + + + True + False + + + 1 + 1 + + + + + True + False + start + False + Protocol: + + + + + + 0 + 6 + + + + + True + False + The character <b>*</b> can be use to match any value. + start + False + Host: + + + + + + 0 + 4 + + + + + True + True + The character <b>*</b> can be use to match any value. + 64 + 34 + The character <b>*</b> can be use to match any value. + Hostname or IP address + + + + 1 + 4 + + + + + True + True + The character <b>*</b> can be use to match any value. + 5 + 4 + 5 + The character <b>*</b> can be use to match any value. + Port + + + + + 1 + 5 + + + + + True + False + The character <b>*</b> can be use to match any value. + start + False + Port: + + + + + + 0 + 5 + + + + + False + True + start + False + SANDBOX + + + 1 + 1 + + + + + False + True + start + False + Pid: + + + + + + 0 + 2 + + + + + False + True + start + False + PID_LABEL + + + 1 + 2 + + + + + False + True + start + False + SCOPE_LABEL + + + 1 + 9 + + + + + True + False + True + top + + + True + False + 0 + -1 + + Any User + + + + False + True + 0 + + + + + Apply + False + True + False + True + 0 + True + + + False + True + 1 + + + + + 1 + 7 + + + + + True + False + True + + + True + False + 0 + -1 + + Any Group + + + + False + True + 0 + + + + + Apply + False + True + False + True + 0 + True + + + False + True + 1 + + + + + 1 + 8 + + + + + False + True + True + + + 1 + 0 + + + + + False + True + The character <b>*</b> can be use to match any value. + start + False + Origin: + + + + + + 0 + 3 + + + + + False + True + True + + + 1 + 3 + + + + + True + False + 0 + 0 + + Permanent + Session + Process + Once + System + + + + 1 + 9 + + + + + True + False + True + + + True + False + 1 + 1 + + Any + TCP + UDP + ICMP + + + + + False + True + 0 + + + + + TLS Only + True + True + False + True + + + False + True + 1 + + + + + 1 + 6 + + + + + False + True + 5 + 1 + + + + + True + False + + + False + True + 2 + + + + + + allow_button + cancel_button + ok_button + + + + +` +} diff --git a/fw-settings/main.go b/fw-settings/main.go index 07142d4..81938d9 100644 --- a/fw-settings/main.go +++ b/fw-settings/main.go @@ -1,189 +1,999 @@ +// XXX: Clarify IsSocks, PID +// XXX: Leak on refresh +// XXX: Find way to share FirewallPrompt introspect xml with extension.js +// XXX: Prompt Only mode with different APPID (debug/dev) +// XXX? inotify refresh passwd/groups +// XXX: Existing prompt bugs: +// > XXX: Dead prompt requests not removed properly +// > XXX: Gtk-WARNING **: /build/gtk+3.0-NmdvYo/gtk+3.0-3.22.11/./gtk/gtktreestore.c:860: Unable to convert from gpointer to gchararray package main import ( + "bufio" + "bytes" + "encoding/xml" "fmt" + "io" + "io/ioutil" "os" + "os/signal" + "strings" + "strconv" "sync" + "syscall" + "time" + "github.com/subgraph/fw-daemon/fw-settings/settings" "github.com/subgraph/fw-daemon/sgfw" + "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/glib" "github.com/gotk3/gotk3/gtk" ) -var fwswin *gtk.Window = nil -var fwsbuilder *builder = nil -var swRulesPermanent *gtk.ScrolledWindow = nil -var swRulesSession *gtk.ScrolledWindow = nil -var swRulesProcess *gtk.ScrolledWindow = nil -var swRulesSystem *gtk.ScrolledWindow = nil +type promptModes uint -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) +const ( + promptModeDisabled promptModes = iota + promptModeOnly + promptModeEnabled +) + +type switcherDirection uint + +const ( + switcherDirectionUp switcherDirection = iota + switcherDirectionDown +) + +type appShortcuts struct { + Accel string + Group string + Title string +} + +type cbPromptAdd func(guid, path, icon, proto string, pid int, ipaddr, hostname string, port, uid, gid int, + origin, timestamp string, is_socks bool, optstring string, sandbox string, action int) bool +type cbPromptRemove func(string) +var cbPromptAddRequest cbPromptAdd = nil +var cbPromptRemoveRequest cbPromptRemove = nil + +const groupFile = "/etc/group" +const userFile = "/etc/passwd" + +type fwApp struct { + *gtk.Application + + forceMenu bool + + Dbus *dbusObject + DbusServer *dbusServer + + promptMode promptModes + prompt *Prompt + + Config *sgfw.FirewallConfigs + Settings *settings.Settings + + winb *builder + win *gtk.ApplicationWindow + repopMutex *sync.Mutex + + swRulesPermanent *gtk.ScrolledWindow + swRulesSession *gtk.ScrolledWindow + swRulesProcess *gtk.ScrolledWindow + swRulesSystem *gtk.ScrolledWindow + swPrompt *gtk.ScrolledWindow + + boxPermanent *gtk.ListBox + boxSession *gtk.ListBox + boxProcess *gtk.ListBox + boxSystem *gtk.ListBox + + rlPermanent *ruleList + rlSession *ruleList + rlProcess *ruleList + rlSystem *ruleList + + btnNewRule *gtk.Button + nbRules *gtk.Notebook + tlStack *gtk.Stack + tlStackSwitcher *gtk.StackSwitcher + gridConfig *gtk.Grid + entrySearch *gtk.SearchEntry + btnSearch *gtk.ToggleButton + revealerSearch *gtk.Revealer + boxAppMenu *gtk.Box + btnAppMenu *gtk.MenuButton + dialog *gtk.MessageDialog + + signalDelete glib.SignalHandle + + lcache string + shortcuts []appShortcuts + + userMap map[int32]string + userIDs []int32 + groupMap map[int32]string + groupIDs []int32 + userMapLock *sync.Mutex + groupMapLock *sync.Mutex + intcount uint + + ozProfiles []string +} + + +/* + * App Setup + */ + +func (fa *fwApp) init() { + fa.Config = &sgfw.FirewallConfigs{} + fa.repopMutex = &sync.Mutex{} + + fa.userMap = make(map[int32]string) + fa.groupMap = make(map[int32]string) + fa.userMapLock = &sync.Mutex{} + fa.groupMapLock = &sync.Mutex{} + + fa.parseArgs() + + if err := fa.cacheUsers(); err != nil { + panic(err) + } + + if err := fa.cacheGroups(); err != nil { + panic(err) + } + + fa.initOZProfiles() + + fa.initGtk() + fa.Settings = settings.Init() + + fa.Run(os.Args) } -func activate(app *gtk.Application) { - win := app.GetActiveWindow() +func (fa *fwApp) parseArgs() { + fa.promptMode = promptModeEnabled + for i := (len(os.Args) - 1); i > 0; i-- { + k := strings.TrimLeft(os.Args[i], "-") + found := false + switch k { + case "prompt-only": + found = true + fa.promptMode = promptModeOnly + case "disable-prompt": + found = true + fa.promptMode = promptModeDisabled + case "gapplication-force-menu": + found = true + fa.forceMenu = true + } + if found { + os.Args = append(os.Args[:i], os.Args[(i+1):]...) + } + } +} + +func (fa *fwApp) initGtk() { + var appFlags glib.ApplicationFlags + appFlags |= glib.APPLICATION_FLAGS_NONE + appFlags |= glib.APPLICATION_CAN_OVERRIDE_APP_ID + //appFlags |= glib.APPLICATION_IS_LAUNCHER + //appFlags |= glib.APPLICATION_IS_SERVICE + app, err := gtk.ApplicationNew("com.subgraph.Firewall.Settings", appFlags)//glib.APPLICATION_FLAGS_NONE) + if err != nil { + panic(fmt.Sprintf("gtk.ApplicationNew() failed: %v", err)) + } + fa.Application = app + + fa.Connect("activate", fa.activate) + fa.Connect("startup", fa.startup) +} + +func (fa *fwApp) activate(app *gtk.Application) { + win := fa.GetActiveWindow() if win != nil { win.Present() return } - populateWin(app, win) -} -var repopMutex = &sync.Mutex{} - -func repopulateWin() { - fmt.Println("Refreshing firewall rule list.") - repopMutex.Lock() - defer repopMutex.Unlock() - win := fwswin + fa.build() + fa.populateWindow() + fa.registerActions() + fa.registerShortcuts() + fa.AddWindow(&fa.win.Window) + fa.win.ShowAll() +} +func (fa *fwApp) startup() { dbus, err := newDbusObject() if err != nil { - failDialog(win, "Failed to connect to dbus system bus: %v", err) + failDialog(&fa.win.Window, "Failed to connect to dbus system bus: %v", err) + } + fa.Dbus = dbus + + if fa.promptMode != promptModeDisabled { + dbuss, _ := newPromptDbusServer() + if fa.promptMode == promptModeOnly && dbuss == nil { + fmt.Println("Prompter already available exiting...") + os.Exit(0) + } + fa.DbusServer = dbuss + if fa.DbusServer == nil { + fa.promptMode = promptModeDisabled + } } - child, err := swRulesPermanent.GetChild() + sigs := make(chan os.Signal) + signal.Notify(sigs, syscall.SIGINT) + go fa.handleSignals(sigs) + + go dbusSignalHandler(fa) +} + +func (fa *fwApp) build() { + fa.buildWindow() + fa.buildAppMenu() +} + +func (fa *fwApp) registerActions() { + anr := glib.SimpleActionNew("new_rule", glib.VARIANT_TYPE_NONE) + anr.Connect("activate", func () { + fa.btnNewRule.Activate() + }) + fa.ActionMap.AddAction(&anr.Action) + + snr := glib.SimpleActionNew("shortcuts", glib.VARIANT_TYPE_NONE) + snr.Connect("activate", func () { + fa.showShortcutsWindow() + }) + fa.ActionMap.AddAction(&snr.Action) + + abnr := glib.SimpleActionNew("about", glib.VARIANT_TYPE_NONE) + abnr.Connect("activate", func() {fa.showAboutDialog()}) + fa.ActionMap.AddAction(&abnr.Action) +/* + hbnr := glib.SimpleActionNew("help", glib.VARIANT_TYPE_NONE) + hbnr.Connect("activate", func() {fmt.Println("UNIMPLEMENTED")}) + fa.ActionMap.AddAction(&hbnr.Action) +*/ + qnr := glib.SimpleActionNew("quit", glib.VARIANT_TYPE_NONE) + qnr.Connect("activate", func() { + fa.win.Close() + }) + fa.ActionMap.AddAction(&qnr.Action) +} + +func (fa *fwApp) registerShortcuts() { + fa.ConnectShortcut("Page_Down", "rules", "Go to next rules views", fa.win.Window, func (win gtk.Window) { + fa.switchRulesItem(switcherDirectionUp) + }) + fa.ConnectShortcut("Page_Up", "rules", "Go to previous rules views", fa.win.Window, func (win gtk.Window) { + fa.switchRulesItem(switcherDirectionDown) + }) + fa.ConnectShortcut("n", "rules", "Create new rule", fa.win.Window, func (win gtk.Window) { + if fa.btnNewRule.GetSensitive() { + fa.btnNewRule.Emit("clicked") + } + }) + fa.ConnectShortcut("f", "rules", "Search for rule", fa.win.Window, func (win gtk.Window) { + if fa.tlStack.GetVisibleChildName() == "rules" { + reveal := fa.revealerSearch.GetRevealChild() + if !reveal { + fa.btnSearch.SetActive(true) + fa.revealerSearch.SetRevealChild(true) + } + fa.entrySearch.Widget.GrabFocus() + } + }) + fa.ConnectShortcut("Page_Down", "general", "Go to the next view", fa.win.Window, func (win gtk.Window) { + fa.switchStackItem(switcherDirectionDown) + }) + fa.ConnectShortcut("Page_Up", "general", "Go to the previous view", fa.win.Window, func (win gtk.Window) { + fa.switchStackItem(switcherDirectionUp) + }) + if fa.promptMode != promptModeDisabled { + fa.RegisterShortcutHelp("space", "general", "Answer first firewall prompt") + } +/* + fa.ConnectShortcut("question", "general", "Show the program help", fa.win.Window, func (win gtk.Window) { + ha := fa.ActionMap.LookupAction("help") + if ha != nil { + ha.Activate(nil) + } + }) +*/ + fa.ConnectShortcut("F1", "general", "Show this help window", fa.win.Window, func (win gtk.Window) { + fa.showShortcutsWindow() + }) + fa.ConnectShortcut("q", "general", "Exit program", fa.win.Window, func (win gtk.Window) { + fa.win.Close() + }) + // Easter Egg + fa.ConnectShortcut("F5", "", "", fa.win.Window, func (win gtk.Window) { + fa.repopulateWindow() + fa.loadConfig(false) + }) +} + +func (fa *fwApp) buildWindow() { + fa.winb = newBuilder("Dialog") + fa.winb.getItems( + "window", &fa.win, + "swRulesPermanent", &fa.swRulesPermanent, + "swRulesSession", &fa.swRulesSession, + "swRulesProcess", &fa.swRulesProcess, + "swRulesSystem", &fa.swRulesSystem, + "btn_new_rule", &fa.btnNewRule, + "rulesnotebook", &fa.nbRules, + "toplevel_stack", &fa.tlStack, + "config_grid", &fa.gridConfig, + "stack_switcher", &fa.tlStackSwitcher, + "prompt_scrollwindow", &fa.swPrompt, + "search_entry", &fa.entrySearch, + "btn_search", &fa.btnSearch, + "search_revealer", &fa.revealerSearch, + "box_app_menu", &fa.boxAppMenu, + "btn_app_menu", &fa.btnAppMenu, + ) + + fa.win.SetIconName("security-medium") + fa.win.SetTitle("Subgraph Firewall Settings") +/* + fa.winb.ConnectSignals(map[string]interface{} { + "on_changed_search": fa.onChangedSearch, + "on_stoped_search": fa.onStopedSearch, + }) +*/ + //fa.swRulesPermanent.Connect("key-press-event", fa.onRulesKeyPress) + fa.entrySearch.Connect("search-changed", fa.onChangedSearch) + fa.entrySearch.Connect("stop-search", fa.onStopedSearch) + fa.btnSearch.Connect("clicked", fa.onButtonSearchClicked) + + fa.btnNewRule.Connect("clicked", fa.showAddRuleDialog) + fa.tlStackSwitcher.Connect("event", fa.onStackChanged) + + fa.win.Connect("configure-event", fa.onWindowConfigure) + + fa.signalDelete, _ = fa.win.Connect("delete-event", fa.onWindowDelete) + + fa.win.SetPosition(gtk.WIN_POS_CENTER) + + if fa.Settings.GetWindowHeight() > 0 && fa.Settings.GetWindowWidth() > 0 { + fa.win.Resize(int(fa.Settings.GetWindowWidth()), int(fa.Settings.GetWindowHeight())) + } + + if fa.Settings.GetWindowTop() > 0 && fa.Settings.GetWindowLeft() > 0 { + fa.win.Move(int(fa.Settings.GetWindowLeft()), int(fa.Settings.GetWindowTop())) + } + + fa.loadConfig(true) + + if fa.promptMode != promptModeDisabled { + fa.tlStack.SetVisibleChildName("prompt") + prompt, err := createPromptView(fa, fa.swPrompt) + if err != nil { + fmt.Println("Unable to create prompter:", err) + os.Exit(1) + } + fa.prompt = prompt + cbPromptAddRequest = fa.prompt.AddRequest + cbPromptRemoveRequest = fa.prompt.RemoveRequest + if fa.promptMode == promptModeOnly { + fa.win.Iconify() + } + } else { + fa.tlStack.SetVisibleChildName("rules") + fa.swPrompt.Destroy() + } +} + +func (fa *fwApp) buildAppMenu() { + ap := glib.MenuNew() + ams := glib.MenuNew() + + ap.Append("_New Rule...", "app.new_rule") + + ams.Append("_Keyboard Shortcuts", "app.shortcuts") + //ams.Append("_Help", "app.help") + ams.Append("_About", "app.about") + ams.Append("_Quit", "app.quit") + ap.AppendSection("", &ams.MenuModel) + + if !fa.forceMenu { + fa.SetAppMenu(&ap.MenuModel) + } + + if fa.forceMenu || !fa.PrefersAppMenu() { + fa.boxAppMenu.SetNoShowAll(false) + fa.boxAppMenu.SetVisible(true) + fa.btnAppMenu.SetMenuModel(&ap.MenuModel) + } +} + + +/* + * Windows + */ + +func (fa *fwApp) showPromptQuit() bool { + fa.win.SetUrgencyHint(true) + fa.win.Deiconify() + fa.win.SetKeepAbove(true) + + res := false + body := "Currently running as the prompt, are you sure you want to exit?" + msg := "The Firewall will stop working as expected!" + fa.dialog = gtk.MessageDialogNewWithMarkup( + fa.win, + gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_WARNING, + gtk.BUTTONS_OK_CANCEL, + "") + fa.dialog.SetMarkup(body) + fa.dialog.SetProperty("secondary-text", msg) + if fa.dialog.Run() == (int)(gtk.RESPONSE_OK) { + res = true + } else { + fa.intcount = 0 + fa.win.SetUrgencyHint(false) + fa.win.SetKeepAbove(false) + } + fa.dialog.Destroy() + return res +} + +func (fa *fwApp) showAddRuleDialog() { + rule := &sgfw.DbusRule{} + rl := &ruleList{app: fa} + rr := &ruleRow{ rl: rl, rule: rule} + rnew := newRuleAdd(rr, DIALOG_MODE_NEW) + rnew.update() + rnew.run("", nil) +} + +func (fa *fwApp) showAboutDialog() { + url := "https://subgraph.com/sgos/" + sfs := "Subgraph Firewall" + t := time.Now() + cs := fmt.Sprintf("%d Subgraph Inc", t.Year()) + + license := "BSD3" + lf := "/usr/share/common-licenses/BSD" + if fa.lcache != "" { + license = license + "\n\n" + fa.lcache + } else { + if _, err := os.Stat(lf); err == nil { + bb, err := ioutil.ReadFile(lf) + if err == nil { + fa.lcache = string(bb) + fa.lcache = strings.Replace(fa.lcache, "The Regents of the University of California", cs, -1) + license = license + "\n\n" + fa.lcache + } + } + } + + ad, _ := gtk.AboutDialogNew() + ad.SetName(sfs) + ad.SetProgramName(sfs) + ad.SetAuthors([]string{"Subgraph Inc"}) + //ad.AddCreditSection("", []string{"- Bruce Leidl", "- David Mirza", "- Stephen Watt", "- Matthieu Lalonde"}) + ad.SetVersion("0.1.0") + ad.SetCopyright(fmt.Sprintf("© %s.", cs)) + ad.SetComments("An interface for the " + sfs) + ad.SetWebsite(url) + ad.SetWebsiteLabel(url) + ad.SetLogoIconName("security-medium") + ad.SetWrapLicense(true) + ad.SetLicenseType(gtk.LICENSE_BSD) + ad.SetLicense(license) + ad.SetWrapLicense(true) + + ad.SetTransientFor(&fa.win.Window) + ad.Run() + ad.Destroy() +} + +func (fa *fwApp) showShortcutsWindow() { + var groups = []string{"general", "rules", "prompt"} + var titles = map[string]string{ + "general": "General", + "rules": "Rules", + "prompt": "Firewall Prompt", + } + xv := new(GtkXMLInterface) + xv.Comment = " interface-requires gtk+ 3.20 " + xv.Requires = &GtkXMLRequires{Lib: "gtk+", Version: "3.20"} + xsw := new(GtkXMLObject) + xsw.Class = "GtkShortcutsWindow" + xsw.ID = "shortcuts_window" + xsw.Properties = []GtkXMLProperty{ + {Name: "modal", Value: "1"}, + {Name: "visible", Value: "1"}, + } + xss := new(GtkXMLObject) + xss.Class = "GtkShortcutsSection" + xss.Properties = []GtkXMLProperty{ + {Name: "visible", Value: "1"}, + {Name: "section-name", Value: "shortcuts"}, + {Name: "max-height", Value: "16"}, + } + xsw.Children = append(xsw.Children, GtkXMLChild{Objects: []*GtkXMLObject{xss}}) + + for _, g := range groups { + xsg := new(GtkXMLObject) + xsg.Class = "GtkShortcutsGroup" + xsg.Properties = []GtkXMLProperty{ + {Name: "title", Value: titles[g], Translatable: "yes"}, + {Name: "visible", Value: "1"}, + } + found := false + for _, sc := range fa.shortcuts { + if sc.Group != g { + continue + } + found = true + xps := new(GtkXMLObject) + xps.Class = "GtkShortcutsShortcut" + xps.Properties = []GtkXMLProperty{ + {Name: "visible", Value: "yes"}, + {Name: "accelerator", Value: sc.Accel}, + {Name: "title", Translatable: "yes", Value: sc.Title}, + } + xsg.Children = append(xsg.Children, GtkXMLChild{Objects: []*GtkXMLObject{xps}}) + } + if found { + xss.Children = append(xss.Children, GtkXMLChild{Objects: []*GtkXMLObject{xsg}}) + } + } + + xv.Objects = append(xv.Objects, xsw) + var buf bytes.Buffer + writer := bufio.NewWriter(&buf) + enc := xml.NewEncoder(writer) + enc.Indent("", " ") + if err := enc.Encode(xv); err != nil { + fmt.Printf("XML ERROR: %+v\n", err) + } else { + //fmt.Println(xml.Header + buf.String()) + var sw *gtk.ShortcutsWindow + b := newBuilderFromString(xml.Header + buf.String()) + b.getItems( + "shortcuts_window", &sw, + ) + sw.Window.SetTransientFor(&fa.win.Window) + sw.Window.SetPosition(gtk.WIN_POS_CENTER_ON_PARENT) + sw.Window.SetModal(true) + fa.AddWindow(&sw.Window) + //sw.ShowAll() + sw.Present() + } +} + + +/* + * Private Utils + */ + +func (fa *fwApp) populateWindow() { + tt, _ := fa.entrySearch.GetText() + if fa.boxPermanent == nil { + fa.boxPermanent, _ = gtk.ListBoxNew() + fa.swRulesPermanent.Add(fa.boxPermanent) + + fa.rlPermanent = newRuleList(fa, fa.boxPermanent, sgfw.RULE_MODE_PERMANENT) + if _, err := fa.Dbus.isEnabled(); err != nil { + failDialog(&fa.win.Window, "Unable is connect to firewall daemon. Is it running?") + } + } + fa.rlPermanent.loadRules(true) + fa.rlPermanent.reloadRules(tt) + + + if fa.boxSession == nil { + fa.boxSession, _ = gtk.ListBoxNew() + fa.swRulesSession.Add(fa.boxSession) + + fa.rlSession = newRuleList(fa, fa.boxSession, sgfw.RULE_MODE_SESSION) + if _, err := fa.Dbus.isEnabled(); err != nil { + failDialog(&fa.win.Window, "Unable is connect to firewall daemon. Is it running?") + } + } + fa.rlSession.loadRules(true) + fa.rlSession.reloadRules(tt) + + + if fa.boxProcess == nil { + fa.boxProcess, _ = gtk.ListBoxNew() + fa.swRulesProcess.Add(fa.boxProcess) + + fa.rlProcess = newRuleList(fa, fa.boxProcess, sgfw.RULE_MODE_PROCESS) + if _, err := fa.Dbus.isEnabled(); err != nil { + failDialog(&fa.win.Window, "Unable is connect to firewall daemon. Is it running?") + } + } + fa.rlProcess.loadRules(true) + fa.rlProcess.reloadRules(tt) + + if fa.boxSystem == nil { + fa.boxSystem, _ = gtk.ListBoxNew() + fa.swRulesSystem.Add(fa.boxSystem) + + fa.rlSystem = newRuleList(fa, fa.boxSystem, sgfw.RULE_MODE_SYSTEM) + if _, err := fa.Dbus.isEnabled(); err != nil { + failDialog(&fa.win.Window, "Unable is connect to firewall daemon. Is it running?") + } + } + fa.rlSystem.loadRules(true) + fa.rlSystem.reloadRules(tt) + + +} + +func (fa *fwApp) repopulateWindow() { + fmt.Println("Refreshing firewall rule list.") + fa.repopMutex.Lock() + defer fa.repopMutex.Unlock() +/* + child, err := fa.swRulesPermanent.GetChild() if err != nil { - failDialog(win, "Unable to clear out permanent rules list display: %v", err) + failDialog(&fa.win.Window, "Unable to clear out permanent rules list display: %v", err) } - swRulesPermanent.Remove(child) + fa.swRulesPermanent.Remove(child) - child, err = swRulesSession.GetChild() + child, err = fa.swRulesSession.GetChild() if err != nil { - failDialog(win, "Unable to clear out session rules list display: %v", err) + failDialog(&fa.win.Window, "Unable to clear out session rules list display: %v", err) } - swRulesSession.Remove(child) + fa.swRulesSession.Remove(child) - child, err = swRulesProcess.GetChild() + child, err = fa.swRulesProcess.GetChild() if err != nil { - failDialog(win, "Unable to clear out process rules list display: %v", err) + failDialog(&fa.win.Window, "Unable to clear out process rules list display: %v", err) } - swRulesProcess.Remove(child) + fa.swRulesProcess.Remove(child) - child, err = swRulesSystem.GetChild() + child, err = fa.swRulesSystem.GetChild() if err != nil { - failDialog(win, "Unable to clear out system rules list display: %v", err) + failDialog(&fa.win.Window, "Unable to clear out system rules list display: %v", err) + } + fa.swRulesSystem.Remove(child) +*/ + if fa.tlStack.GetVisibleChildName() != "rules" && fa.promptMode == promptModeDisabled { + stack := fa.tlStack.GetChildByName("rules") + err := fa.tlStack.ChildSetProperty(stack, "needs-attention", true) + if err != nil { + fmt.Println("Error setting stack attention") + } } - swRulesSystem.Remove(child) - boxPermanent, _ := gtk.ListBoxNew() - swRulesPermanent.Add(boxPermanent) + fa.populateWindow() + fa.win.ShowAll() +} - boxSession, _ := gtk.ListBoxNew() - swRulesSession.Add(boxSession) +func (fa *fwApp) switchRulesItem(dir switcherDirection) { + focus := (fa.nbRules.Container.GetFocusChild() != nil || fa.nbRules.HasFocus()) + if focus { + return + } + if fa.tlStack.GetVisibleChildName() != "rules" { + return + } + if dir == switcherDirectionUp { + if fa.nbRules.GetNPages() == (fa.nbRules.GetCurrentPage() + 1) { + fa.nbRules.SetCurrentPage(0) + } else { + fa.nbRules.NextPage() + } + } else { + if fa.nbRules.GetCurrentPage() == 0 { + fa.nbRules.SetCurrentPage(fa.nbRules.GetNPages() - 1) + } else { + fa.nbRules.PrevPage() + } + } +} - boxProcess, _ := gtk.ListBoxNew() - swRulesProcess.Add(boxProcess) +func (fa *fwApp) switchStackItem(dir switcherDirection) { + stacks := []string{"prompt", "rules", "config"} + stacksByName := map[string]int{ + "prompt": 0, + "rules": 1, + "config": 2, + } + if fa.promptMode == promptModeDisabled { + stacks = stacks[1:] + delete(stacksByName, "prompt") + stacksByName["rules"] = 0 + stacksByName["config"] = 1 + } + idx := stacksByName[fa.tlStack.GetVisibleChildName()] + if dir == switcherDirectionUp { + idx = idx - 1 + if idx < 0 { + idx = len(stacks) - 1 + } + fa.tlStack.SetVisibleChildFull(stacks[idx], gtk.STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT) + } else { + idx = idx + 1 + if idx >= len(stacks) { + idx = 0 + } + fa.tlStack.SetVisibleChildFull(stacks[idx], gtk.STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT) + } + fa.onStackChanged() +} - boxSystem, _ := gtk.ListBoxNew() - swRulesSystem.Add(boxSystem) - rlPermanent := newRuleList(dbus, win, boxPermanent) - if _, err := dbus.isEnabled(); err != nil { - failDialog(win, "Unable is connect to firewall daemon. Is it running?") +/* + * Handlers + */ + +func (fa *fwApp) handleSignals(c <-chan os.Signal) { + for { + sig := <-c + switch sig { + case syscall.SIGINT: + if fa.intcount == 0 { + glib.IdleAdd(func () bool { + fa.win.Close() + return false + }) + } else { + if fa.signalDelete != 0 { + fa.win.HandlerDisconnect(fa.signalDelete) + } + fa.win.Destroy() + } + fa.intcount++ + } } - rlPermanent.loadRules(sgfw.RULE_MODE_PERMANENT) +} + +func (fa *fwApp) handleRefreshRules() { + fa.repopulateWindow() +} - rlSession := newRuleList(dbus, win, boxSession) - if _, err := dbus.isEnabled(); err != nil { - failDialog(win, "Unable is connect to firewall daemon. Is it running?") +func (fa *fwApp) handleRefreshConfig() { + fa.loadConfig(false) +} + +func (fa *fwApp) onWindowConfigure() { + w, h := fa.win.GetSize() + fa.Settings.SetWindowHeight(uint(h)) + fa.Settings.SetWindowWidth(uint(w)) + l, t := fa.win.GetPosition() + fa.Settings.SetWindowTop(uint(t)) + fa.Settings.SetWindowLeft(uint(l)) +} + +func (fa *fwApp) onWindowDelete() bool { + if fa.promptMode != promptModeDisabled { + if !fa.showPromptQuit() { + return true + } } - rlSession.loadRules(sgfw.RULE_MODE_SESSION) + return false +} - rlProcess := newRuleList(dbus, win, boxProcess) - if _, err := dbus.isEnabled(); err != nil { - failDialog(win, "Unable is connect to firewall daemon. Is it running?") +func (fa *fwApp) onStackChanged() { +tn := fa.tlStack.GetVisibleChildName() + nra := fa.ActionMap.LookupAction("new_rule") + if tn == "rules" { + fa.btnNewRule.SetSensitive(true) + nra.SetProperty("enabled", true) + stack := fa.tlStack.GetChildByName("rules") + err := fa.tlStack.ChildSetProperty(stack, "needs-attention", false) + if err != nil { + fmt.Println("Error unsetting stack attention") + } + } else if tn == "prompt" { + fa.btnNewRule.SetSensitive(true) + nra.SetProperty("enabled", true) + stack := fa.tlStack.GetChildByName("prompt") + err := fa.tlStack.ChildSetProperty(stack, "needs-attention", false) + if err != nil { + fmt.Println("Error unsetting stack attention") + } + } else { + fa.btnNewRule.SetSensitive(false) + nra.SetProperty("enabled", false) } - rlProcess.loadRules(sgfw.RULE_MODE_PROCESS) - rlSystem := newRuleList(dbus, win, boxSystem) - if _, err := dbus.isEnabled(); err != nil { - failDialog(win, "Unable is connect to firewall daemon. Is it running?") + if fa.prompt != nil && tn != "prompt"{ + pstack := fa.tlStack.GetChildByName("prompt") + nag, _ := fa.tlStack.ChildGetProperty(pstack, "needs-attention", glib.TYPE_BOOLEAN) + if fa.prompt.HasItems() && !nag.(bool) { + err := fa.tlStack.ChildSetProperty(pstack, "needs-attention", true) + if err != nil { + fmt.Println("Error unsetting stack attention") + } + } } - rlSystem.loadRules(sgfw.RULE_MODE_SYSTEM) +} - loadConfig(win, fwsbuilder, dbus) - // app.AddWindow(win) - win.ShowAll() +func (fa *fwApp) onChangedSearch(entry *gtk.SearchEntry) { + fa.repopMutex.Lock() + defer fa.repopMutex.Unlock() + tt, _ := entry.Entry.GetText() + fa.rlPermanent.reloadRules(tt) + fa.rlSession.reloadRules(tt) + fa.rlProcess.reloadRules(tt) + fa.rlSystem.reloadRules(tt) } -func populateWin(app *gtk.Application, win *gtk.Window) { - b := newBuilder("Dialog") - fwsbuilder = b - b.getItems( - "window", &win, - "swRulesPermanent", &swRulesPermanent, - "swRulesSession", &swRulesSession, - "swRulesProcess", &swRulesProcess, - "swRulesSystem", &swRulesSystem, - ) - //win.SetIconName("security-high-symbolic") - win.SetIconName("security-medium") +func (fa *fwApp) onStopedSearch() { + fa.entrySearch.Entry.SetText("") + fa.btnSearch.SetActive(false) + fa.revealerSearch.SetRevealChild(false) +} - boxPermanent, _ := gtk.ListBoxNew() - swRulesPermanent.Add(boxPermanent) +func (fa *fwApp) onButtonSearchClicked() { + reveal := fa.revealerSearch.GetRevealChild() + if reveal { + fa.entrySearch.SetText("") + } + fa.btnSearch.SetActive(!reveal) + fa.revealerSearch.SetRevealChild(!reveal) + fa.entrySearch.Widget.GrabFocus() +} - boxSession, _ := gtk.ListBoxNew() - swRulesSession.Add(boxSession) +func (fa *fwApp) onRulesKeyPress(i interface{}, e *gdk.Event) bool { + ek := gdk.EventKeyNewFromEvent(e) + reveal := fa.revealerSearch.GetRevealChild() + if !reveal { + fa.btnSearch.SetActive(true) + fa.revealerSearch.SetRevealChild(true) + } + fa.entrySearch.GrabFocusWithoutSelecting() + fa.entrySearch.SetText(string(ek.KeyVal())) + return true +} - boxProcess, _ := gtk.ListBoxNew() - swRulesProcess.Add(boxProcess) - boxSystem, _ := gtk.ListBoxNew() - swRulesSystem.Add(boxSystem) +/* + * Users, Groups + */ - dbus, err := newDbusObject() +func (fa *fwApp) cacheUsers() error { + f, err := os.Open(userFile) if err != nil { - failDialog(win, "Failed to connect to dbus system bus: %v", err) + return err } + defer f.Close() + fa.userMapLock.Lock() + defer fa.userMapLock.Unlock() + + readColonFile(f, func (line []byte) { + t := strings.Split(string(line), ":") + id, _ := strconv.ParseInt(t[2], 10, 32) + fa.userMap[int32(id)] = t[0] + fa.userIDs = append(fa.userIDs, int32(id)) + }) + return nil - rlPermanent := newRuleList(dbus, win, boxPermanent) - if _, err := dbus.isEnabled(); err != nil { - failDialog(win, "Unable is connect to firewall daemon. Is it running?") - } - rlPermanent.loadRules(sgfw.RULE_MODE_PERMANENT) +} - rlSession := newRuleList(dbus, win, boxSession) - if _, err := dbus.isEnabled(); err != nil { - failDialog(win, "Unable is connect to firewall daemon. Is it running?") +func (fa *fwApp) cacheGroups() error { + f, err := os.Open(groupFile) + if err != nil { + return err } - rlSession.loadRules(sgfw.RULE_MODE_SESSION) + defer f.Close() + fa.groupMapLock.Lock() + defer fa.groupMapLock.Unlock() + + readColonFile(f, func (line []byte) { + t := strings.Split(string(line), ":") + id, _ := strconv.ParseInt(t[2], 10, 32) + fa.groupMap[int32(id)] = t[0] + fa.groupIDs = append(fa.groupIDs, int32(id)) + }) + return nil +} + + +/* + * Exported + */ + +func (fa *fwApp) RegisterShortcutHelp(accel, group, title string) { + fa.shortcuts = append(fa.shortcuts, appShortcuts{Accel: accel, Group: group, Title: title}) +} - rlProcess := newRuleList(dbus, win, boxProcess) - if _, err := dbus.isEnabled(); err != nil { - failDialog(win, "Unable is connect to firewall daemon. Is it running?") +func (fa *fwApp) ConnectShortcut(accel, group, title string, w gtk.Window, action func(gtk.Window)) { + if group != "" && title != "" { + fa.RegisterShortcutHelp(accel, group, title) } - rlProcess.loadRules(sgfw.RULE_MODE_PROCESS) + gr, _ := gtk.AccelGroupNew() + key, mod := gtk.AcceleratorParse(accel) + + // Do not remove the closure here - there is a limitation + // in gtk that makes it necessary to have different functions for different accelerator groups + gr.Connect(key, mod, gtk.ACCEL_VISIBLE, func() { + action(w) + }) + + w.AddAccelGroup(gr) + w.Connect("delete-event", func () bool { + w.RemoveAccelGroup(gr) + return false + }) +} - rlSystem := newRuleList(dbus, win, boxSystem) - if _, err := dbus.isEnabled(); err != nil { - failDialog(win, "Unable is connect to firewall daemon. Is it running?") +func (fa *fwApp) LookupUsername(uid int32) string { + if uid == -1 { + return "any" } - rlSystem.loadRules(sgfw.RULE_MODE_SYSTEM) + fa.userMapLock.Lock() + defer fa.userMapLock.Unlock() - loadConfig(win, b, dbus) - app.AddWindow(win) - fwswin = win - win.ShowAll() + if val, ok := fa.userMap[uid]; ok { + return val + } + return "unknown" } -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)) +func (fa *fwApp) LookupGroup(gid int32) string { + if gid == -1 { + return "any" } - app.Connect("activate", activate) + fa.groupMapLock.Lock() + defer fa.groupMapLock.Unlock() - _, err = newDbusServer() + if val, ok := fa.groupMap[gid]; ok { + return val + } + return "unknown" +} - if err != nil { - panic(fmt.Sprintf("Error initializing Dbus server: %v", err)) + +/* + * Global Utils + */ + +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 warnDialog(parent *gtk.Window, format string, args ...interface{}) { + d := gtk.MessageDialogNew(parent, 0, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, + format, args...) + d.Run() + d.Destroy() +} + +// readColonFile parses r as an /etc/group or /etc/passwd style file, running +// fn for each row. readColonFile returns a value, an error, or (nil, nil) if +// the end of the file is reached without a match. +func readColonFile(r io.Reader, fn func(line []byte)) (v interface{}, err error) { + bs := bufio.NewScanner(r) + for bs.Scan() { + line := bs.Bytes() + // There's no spec for /etc/passwd or /etc/group, but we try to follow + // the same rules as the glibc parser, which allows comments and blank + // space at the beginning of a line. + line = bytes.TrimSpace(line) + if len(line) == 0 || line[0] == '#' { + continue + } + fn(line) } + return nil, bs.Err() +} + - app.Run(os.Args) +/* + * Main + */ + +func main() { + app := &fwApp{} + app.init() } diff --git a/fw-settings/oz-sandboxes.go b/fw-settings/oz-sandboxes.go new file mode 100644 index 0000000..0e7df29 --- /dev/null +++ b/fw-settings/oz-sandboxes.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "os" + + "github.com/subgraph/oz" +) + +var ozProfiles oz.Profiles + +func init() { + c, err := oz.LoadConfig(oz.DefaultConfigPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to read oz config...") + os.Exit(1) + } + p, err := oz.LoadProfiles(c.ProfileDir) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to read oz profiles...") + os.Exit(1) + } + ozProfiles = p +} + +func (fa *fwApp) initOZProfiles() { + for _, p := range ozProfiles { + // XXX: This actually should match against sgfw's opened sockets + switch { + case string(p.Networking.Nettype) == "host": + fallthrough + case len(p.Networking.Sockets) == 0: + continue + default: + fa.ozProfiles = append(fa.ozProfiles, p.Name) + } + } +} diff --git a/fw-settings/prompt.go b/fw-settings/prompt.go new file mode 100644 index 0000000..23f44b5 --- /dev/null +++ b/fw-settings/prompt.go @@ -0,0 +1,901 @@ +package main + +import ( + "errors" + "fmt" + "log" + spath "path" + "strings" + "strconv" + "sync" + "time" + + "github.com/subgraph/fw-daemon/sgfw" + + "github.com/gotk3/gotk3/gdk" + "github.com/gotk3/gotk3/glib" + "github.com/gotk3/gotk3/gtk" +) + +type Prompt struct { + app *fwApp + tv *gtk.TreeView + ts *gtk.TreeStore + stack *gtk.Stack + pecols []*gtk.TreeViewColumn + pncol *gtk.TreeViewColumn + promptLock *sync.Mutex + recentLock *sync.Mutex + config *sgfw.FirewallConfigs + recentlyRemoved []string +} + +const ( + COL_NO_NREFS = iota + COL_NO_ICON_PIXBUF + COL_NO_GUID + COL_NO_PATH + COL_NO_SANDBOX + COL_NO_ICON + COL_NO_PROTO + COL_NO_PID + COL_NO_DSTIP + COL_NO_HOSTNAME + COL_NO_PORT + COL_NO_UID + COL_NO_GID + COL_NO_ORIGIN + COL_NO_TIMESTAMP + COL_NO_IS_SOCKS + COL_NO_OPTSTRING + COL_NO_ACTION + COL_NO_FILLER + COL_NO_LAST +) + + +type ruleColumns struct { + nrefs int + Path string + Sandbox string + GUID string + Icon string + Proto string + Pid int + Target string + Hostname string + Port int + UID int + GID int + Uname string + Gname string + Origin string + Timestamp string + IsSocks bool + ForceTLS bool + Scope int +} + +func createPromptView(app *fwApp, sw *gtk.ScrolledWindow) (*Prompt, error) { + var err error + p := &Prompt{} + p.app = app + p.promptLock = &sync.Mutex{} + p.recentLock = &sync.Mutex{} + + p.tv, err = gtk.TreeViewNew() + + if err != nil { + return nil, err + } + + p.tv.SetSizeRequest(300, 300) + p.tv.SetHeadersClickable(true) + p.tv.SetEnableSearch(false) + + p.tv.AppendColumn(createColumnText("#", COL_NO_NREFS)) + p.tv.AppendColumn(createColumnImg("", COL_NO_ICON_PIXBUF)) + + guidcol := createColumnText("GUID", COL_NO_GUID) + guidcol.SetVisible(false) + p.tv.AppendColumn(guidcol) + + p.tv.AppendColumn(createColumnText("Path", COL_NO_PATH)) + + sbcol := createColumnText("Sandbox", COL_NO_SANDBOX) + sbcol.SetVisible(false) + p.tv.AppendColumn(sbcol) + + icol := createColumnText("Icon", COL_NO_ICON) + icol.SetVisible(false) + p.tv.AppendColumn(icol) + + var pecol *gtk.TreeViewColumn + p.tv.AppendColumn(createColumnText("Protocol", COL_NO_PROTO)) + pecol = createColumnText("PID", COL_NO_PID) + p.tv.AppendColumn(pecol) + p.pecols = append(p.pecols, pecol) + p.tv.AppendColumn(createColumnText("IP Address", COL_NO_DSTIP)) + pecol = createColumnText("Hostname", COL_NO_HOSTNAME) + pecol.SetMinWidth(64) + p.tv.AppendColumn(pecol) + p.tv.AppendColumn(createColumnText("Port", COL_NO_PORT)) + pecol = createColumnText("UID", COL_NO_UID) + p.tv.AppendColumn(pecol) + p.pecols = append(p.pecols, pecol) + pecol = createColumnText("GID", COL_NO_GID) + p.tv.AppendColumn(pecol) + p.pecols = append(p.pecols, pecol) + pecol = createColumnText("Origin", COL_NO_ORIGIN) + p.tv.AppendColumn(pecol) + p.pecols = append(p.pecols, pecol) + pecol = createColumnText("Timestamp", COL_NO_TIMESTAMP) + p.tv.AppendColumn(pecol) + p.pecols = append(p.pecols, pecol) + + scol := createColumnText("Is SOCKS", COL_NO_IS_SOCKS) + scol.SetVisible(false) + p.tv.AppendColumn(scol) + + pecol = createColumnText("Details", COL_NO_OPTSTRING) + p.tv.AppendColumn(pecol) + p.pecols = append(p.pecols, pecol) + + acol := createColumnText("Scope", COL_NO_ACTION) + acol.SetVisible(false) + p.tv.AppendColumn(acol) + + pncol := createColumnImg("", COL_NO_FILLER) + pncol.SetVisible(false) + pncol.SetSortIndicator(false) + p.tv.AppendColumn(pncol) + p.pncol = pncol + + p.togglePECols() + + p.ts = createTreeStore(true) + + p.tv.SetModel(p.ts) + p.tv.Connect("row-activated", func() { + p.promptLock.Lock() + seldata, _, _, err := p.getSelectedRule() + p.promptLock.Unlock() + if err != nil { + warnDialog(&p.app.win.Window, "Unexpected error reading selected rule: " + err.Error() + "\n" + fmt.Sprintf("%+v", seldata)) + return + } + + rl := &ruleList{app: p.app} + target := seldata.Hostname + if target == "" { + target = seldata.Target + } + rr := &ruleRow{ rl: rl, rule: &sgfw.DbusRule{ + Path: seldata.Path, + Sandbox: seldata.Sandbox, + Pid: uint32(seldata.Pid), + UID: int32(seldata.UID), + GID: int32(seldata.GID), + Target: strings.Join([]string{target, strconv.Itoa(seldata.Port)}, ":"), + Proto: seldata.Proto, + Origin: seldata.Origin, + IsSocks: seldata.IsSocks, + }} + redit := newRuleAdd(rr, DIALOG_MODE_PROMPT) + redit.update() + redit.run(seldata.GUID, p.buttonAction) + return + }) + + p.app.appendConfigCallback(p.togglePECols) + + sw.SetSizeRequest(600, 400) + p.createShortcuts() + sw.Add(p.tv) + return p, nil +} + +func (p *Prompt) HasItems() bool { + return p.ts.IterNChildren(nil) > 0 +} + +func (p *Prompt) togglePECols() { + v := p.app.Config.PromptExpanded + for _, pc := range p.pecols { + pc.SetVisible(v) + } + p.pncol.SetVisible(!v) +} + +func (p *Prompt) createShortcuts() { + // We register here since the shortcuts are bound in an ephemeral window + p.app.RegisterShortcutHelp("a", "prompt", "Allow") + p.app.RegisterShortcutHelp("d Escape", "prompt", "Deny") + p.app.RegisterShortcutHelp("c", "prompt", "Cancel") + p.app.RegisterShortcutHelp("h", "prompt", "Select the hostname/IP entry") + p.app.RegisterShortcutHelp("p", "prompt", "Select the port entry") + p.app.RegisterShortcutHelp("o", "prompt", "Select protocol") + p.app.RegisterShortcutHelp("t", "prompt", "Toggle allow TLS only") + p.app.RegisterShortcutHelp("s", "prompt", "Select scope") + p.app.RegisterShortcutHelp("u", "prompt", "Toggle apply UID") + p.app.RegisterShortcutHelp("g", "prompt", "Toggle apply GID") + + p.app.ConnectShortcut("space", "", "", p.app.win.Window, func (win gtk.Window) { + vis := p.app.tlStack.GetVisibleChildName() + iter, found := p.ts.GetIterFirst() + if iter == nil || found == false && vis != "prompt" { + return + } + if vis != "prompt" { + p.app.tlStack.SetVisibleChildFull("prompt", gtk.STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT) + p.app.onStackChanged() + } + pi, _ := p.ts.GetPath(iter) + if pi != nil { + p.tv.SetCursor(pi, nil, false) + p.tv.Emit("row-activated") + } + }) +} + +func createColumnImg(title string, id int) *gtk.TreeViewColumn { + cellRenderer, err := gtk.CellRendererPixbufNew() + if err != nil { + log.Fatal("Unable to create image cell renderer:", err) + } + + column, err := gtk.TreeViewColumnNewWithAttribute(title, cellRenderer, "pixbuf", id) + if err != nil { + log.Fatal("Unable to create cell column:", err) + } + + return column +} + +func createColumnText(title string, id int) *gtk.TreeViewColumn { + cellRenderer, err := gtk.CellRendererTextNew() + if err != nil { + log.Fatal("Unable to create text cell renderer:", err) + } + + column, err := gtk.TreeViewColumnNewWithAttribute(title, cellRenderer, "text", id) + if err != nil { + log.Fatal("Unable to create cell column:", err) + } + + column.SetSortColumnID(id) + column.SetResizable(true) + return column +} + +func createTreeStore(general bool) *gtk.TreeStore { + colData := []glib.Type{glib.TYPE_INT, glib.TYPE_OBJECT, glib.TYPE_STRING, glib.TYPE_STRING, glib.TYPE_STRING, glib.TYPE_STRING, glib.TYPE_STRING, glib.TYPE_INT, glib.TYPE_STRING, + glib.TYPE_STRING, glib.TYPE_INT, glib.TYPE_INT, glib.TYPE_INT, glib.TYPE_STRING, glib.TYPE_STRING, glib.TYPE_INT, glib.TYPE_STRING, glib.TYPE_INT, glib.TYPE_OBJECT} + + treeStore, err := gtk.TreeStoreNew(colData...) + if err != nil { + log.Fatal("Unable to create list store:", err) + } + + return treeStore +} + + +func (p *Prompt) addRequestInc(guid, path, icon, proto string, pid int, ipaddr, hostname string, port, uid, gid int, + origin, timestamp string, is_socks bool, optstring string, sandbox string, action int) bool { + duplicated := false + + p.promptLock.Lock() + defer p.promptLock.Unlock() + for ridx := 0; ridx < p.ts.IterNChildren(nil); ridx++ { + rule, iter, err := p.getRuleByIdx(ridx, -1) + if err != nil { + break + // XXX: not compared: optstring/sandbox + } else if (rule.Path == path) && (rule.Proto == proto) && (rule.Pid == pid) && (rule.Target == ipaddr) && (rule.Hostname == hostname) && + (rule.Port == port) && (rule.UID == uid) && (rule.GID == gid) && (rule.Origin == origin) && (rule.IsSocks == is_socks) { + rule.nrefs++ + + err := p.ts.SetValue(iter, 0, rule.nrefs) + if err != nil { + fmt.Println("Error creating duplicate firewall prompt entry:", err) + break + } + + duplicated = true + subiter := p.ts.Append(iter) + p.storeNewEntry(subiter, guid, path, sandbox, icon, proto, pid, ipaddr, hostname, port, uid, gid, origin, timestamp, is_socks, optstring, action) + break + } + + } + + return duplicated +} + +func (p *Prompt) AddRequest(guid, path, icon, proto string, pid int, ipaddr, hostname string, port, uid, gid int, + origin, timestamp string, is_socks bool, optstring string, sandbox string, action int) bool { + if p.ts == nil { + waitTimes := []int{1, 2, 5, 10} + + if p.ts == nil { + fmt.Println("SGFW prompter was not ready to receive firewall request... waiting") + + for _, wtime := range waitTimes { + time.Sleep(time.Duration(wtime) * time.Second) + + if p.ts != nil { + break + } + + fmt.Println("SGFW prompter is still waiting...") + } + } + } + + if p.ts == nil { + log.Fatal("SGFW prompter GUI failed to load for unknown reasons") + } + + if p.addRequestInc(guid, path, icon, proto, pid, ipaddr, hostname, port, uid, gid, origin, timestamp, is_socks, optstring, sandbox, action) { + fmt.Println("Request was duplicate: ", guid) + p.promptLock.Lock() + p.toggleHover() + p.promptLock.Unlock() + return true + } + + p.promptLock.Lock() + defer p.promptLock.Unlock() + + iter := p.ts.Append(nil) + p.storeNewEntry(iter, guid, path, sandbox, icon, proto, pid, ipaddr, hostname, port, uid, gid, origin, timestamp, is_socks, optstring, action) + + p.toggleHover() + return true +} + +// Needs to be locked by caller +func (p *Prompt)storeNewEntry(iter *gtk.TreeIter, guid, path, sandbox, icon, proto string, pid int, ipaddr, hostname string, port, uid, gid int, origin, + timestamp string, is_socks bool, optstring string, action int) { + var colVals = [COL_NO_LAST]interface{}{} + + if is_socks { + if (optstring != "") && (strings.Index(optstring, "SOCKS") == -1) { + optstring = "SOCKS5 / " + optstring + } else if optstring == "" { + optstring = "SOCKS5" + } + } + + colVals[COL_NO_NREFS] = 1 + colVals[COL_NO_ICON_PIXBUF] = nil + colVals[COL_NO_GUID] = guid + colVals[COL_NO_PATH] = path + colVals[COL_NO_SANDBOX] = sandbox + colVals[COL_NO_ICON] = icon + colVals[COL_NO_PROTO] = proto + colVals[COL_NO_PID] = pid + + if ipaddr == "" { + colVals[COL_NO_DSTIP] = "---" + } else { + colVals[COL_NO_DSTIP] = ipaddr + } + + colVals[COL_NO_HOSTNAME] = hostname + colVals[COL_NO_PORT] = port + colVals[COL_NO_UID] = uid + colVals[COL_NO_GID] = gid + colVals[COL_NO_ORIGIN] = origin + colVals[COL_NO_TIMESTAMP] = timestamp + colVals[COL_NO_IS_SOCKS] = 0 + + if is_socks { + colVals[COL_NO_IS_SOCKS] = 1 + } + + colVals[COL_NO_OPTSTRING] = optstring + colVals[COL_NO_ACTION] = action + colVals[COL_NO_FILLER] = nil + + itheme, err := gtk.IconThemeGetDefault() + if err != nil { + log.Fatal("Could not load default icon theme:", err) + } + + in := []string{spath.Base(path)} + if sandbox != "" { + in = append([]string{sandbox}, in...) + } + if icon != "" { + in = append(in, icon) + } + in = append(in, "terminal") + if path == "[unknown]" { + in = []string{"image-missing"} + } + + for _, ia := range in { + pb, _ := itheme.LoadIcon(ia, int(gtk.ICON_SIZE_BUTTON), gtk.ICON_LOOKUP_GENERIC_FALLBACK) + if pb != nil { + colVals[COL_NO_ICON_PIXBUF] = pb + break + } + } + + pb, err := gdk.PixbufNew(gdk.COLORSPACE_RGB, true, 8, 24, 24) + if err != nil { + log.Println("Error creating blank icon:", err) + } else { + colVals[COL_NO_FILLER] = pb + + img, err := gtk.ImageNewFromPixbuf(pb) + if err != nil { + log.Println("Error creating image from pixbuf:", err) + } else { + img.Clear() + pb = img.GetPixbuf() + colVals[COL_NO_FILLER] = pb + } + } + + for n := 0; n < len(colVals); n++ { + err := p.ts.SetValue(iter, n, colVals[n]) + if err != nil { + log.Fatal("Unable to add row:", err) + } + } + + return +} + +func (p *Prompt) getRuleByIdx(idx, subidx int) (ruleColumns, *gtk.TreeIter, error) { + rule := ruleColumns{} + tpath := fmt.Sprintf("%d", idx) + + if subidx != -1 { + tpath = fmt.Sprintf("%d:%d", idx, subidx) + } + + path, err := gtk.TreePathNewFromString(tpath) + if err != nil { + return rule, nil, err + } + + iter, err := p.ts.GetIter(path) + if err != nil { + return rule, nil, err + } + + rule.nrefs, err = p.lsGetInt(iter, COL_NO_NREFS) + if err != nil { + return rule, nil, err + } + + rule.GUID, err = p.lsGetStr(iter, COL_NO_GUID) + if err != nil { + return rule, nil, err + } + + rule.Path, err = p.lsGetStr(iter, COL_NO_PATH) + if err != nil { + return rule, nil, err + } + + rule.Sandbox, err = p.lsGetStr(iter, COL_NO_SANDBOX) + if err != nil { + return rule, nil, err + } + + rule.Icon, err = p.lsGetStr(iter, COL_NO_ICON) + if err != nil { + return rule, nil, err + } + + rule.Proto, err = p.lsGetStr(iter, COL_NO_PROTO) + if err != nil { + return rule, nil, err + } + + rule.Pid, err = p.lsGetInt(iter, COL_NO_PID) + if err != nil { + return rule, nil, err + } + + rule.Target, err = p.lsGetStr(iter, COL_NO_DSTIP) + if err != nil { + return rule, nil, err + } + + rule.Hostname, err = p.lsGetStr(iter, COL_NO_HOSTNAME) + if err != nil { + return rule, nil, err + } + + rule.Port, err = p.lsGetInt(iter, COL_NO_PORT) + if err != nil { + return rule, nil, err + } + + rule.UID, err = p.lsGetInt(iter, COL_NO_UID) + if err != nil { + return rule, nil, err + } + + rule.GID, err = p.lsGetInt(iter, COL_NO_GID) + if err != nil { + return rule, nil, err + } + + rule.Origin, err = p.lsGetStr(iter, COL_NO_ORIGIN) + if err != nil { + return rule, nil, err + } + + rule.Timestamp, err = p.lsGetStr(iter, COL_NO_TIMESTAMP) + if err != nil { + return rule, nil, err + } + + rule.IsSocks = false + is_socks, err := p.lsGetInt(iter, COL_NO_IS_SOCKS) + if err != nil { + return rule, nil, err + } + + if is_socks != 0 { + rule.IsSocks = true + } + + rule.Scope, err = p.lsGetInt(iter, COL_NO_ACTION) + if err != nil { + return rule, nil, err + } + + return rule, iter, nil +} + +func (p *Prompt) lsGetInt(iter *gtk.TreeIter, idx int) (int, error) { + val, err := p.ts.GetValue(iter, idx) + if err != nil { + return 0, err + } + + ival, err := val.GoValue() + if err != nil { + return 0, err + } + + return ival.(int), nil +} + + +func (p *Prompt) lsGetStr(iter *gtk.TreeIter, idx int) (string, error) { + val, err := p.ts.GetValue(iter, idx) + if err != nil { + return "", err + } + + sval, err := val.GetString() + if err != nil { + return "", err + } + + return sval, nil +} + +func (p *Prompt) toggleHover() { + nitems := p.ts.IterNChildren(nil) + stack := p.app.tlStack.GetChildByName("prompt") + + if nitems > 0 { + if p.app.Settings.GetToplevelPrompt() { + //p.win.SetModal(true) + p.app.win.Deiconify() + p.app.win.SetKeepAbove(true) + p.app.win.Stick() + } + p.app.win.SetUrgencyHint(true) + p.app.win.Present() + if p.app.tlStack.GetVisibleChildName() != "prompt" { + err := p.app.tlStack.ChildSetProperty(stack, "needs-attention", true) + if err != nil { + fmt.Println("Error setting stack attention") + } + } + } else { + //p.win.SetModal(false) + p.app.win.SetUrgencyHint(false) + if p.app.Settings.GetToplevelPrompt() { + p.app.win.SetKeepAbove(false) + p.app.win.Unstick() + } + p.app.tlStack.ChildSetProperty(stack, "needs-attention", false) + } +} + +// Needs to be locked by the caller +func (p *Prompt) getSelectedRule() (ruleColumns, int, int, error) { + rule := ruleColumns{} + + sel, err := p.tv.GetSelection() + if err != nil { + return rule, -1, -1, err + } + + rows := sel.GetSelectedRows(p.ts) + + if rows.Length() <= 0 { + return rule, -1, -1, errors.New("no selection was made") + } + + rdata := rows.NthData(0) + tpath := rdata.(*gtk.TreePath).String() + + subidx := -1 + ptoks := strings.Split(tpath, ":") + + if len(ptoks) > 2 { + return rule, -1, -1, errors.New("internal error parsing selected item tree path") + } else if len(ptoks) == 2 { + subidx, err = strconv.Atoi(ptoks[1]) + if err != nil { + return rule, -1, -1, err + } + tpath = ptoks[0] + } + + lIndex, err := strconv.Atoi(tpath) + if err != nil { + return rule, -1, -1, err + } + + // fmt.Printf("lindex = %d : %d\n", lIndex, subidx) + rule, _, err = p.getRuleByIdx(lIndex, subidx) + if err != nil { + return rule, -1, -1, err + } + + return rule, lIndex, subidx, nil +} + +// Needs to be locked by the caller +func (p *Prompt) numSelections() int { + sel, err := p.tv.GetSelection() + if err != nil { + return -1 + } + + rows := sel.GetSelectedRows(p.ts) + return int(rows.Length()) +} + + +func (p *Prompt) removeSelectedRule(idx, subidx int) error { + fmt.Printf("XXX: attempting to remove idx = %v, %v\n", idx, subidx) + ppathstr := fmt.Sprintf("%d", idx) + pathstr := ppathstr + + if subidx > -1 { + pathstr = fmt.Sprintf("%d:%d", idx, subidx) + } + + iter, err := p.ts.GetIterFromString(pathstr) + if err != nil { + return err + } + + nchildren := p.ts.IterNChildren(iter) + + if nchildren >= 1 { + firstpath := fmt.Sprintf("%d:0", idx) + citer, err := p.ts.GetIterFromString(firstpath) + if err != nil { + return err + } + + gnrefs, err := p.ts.GetValue(iter, COL_NO_NREFS) + if err != nil { + return err + } + + vnrefs, err := gnrefs.GoValue() + if err != nil { + return err + } + + nrefs := vnrefs.(int) - 1 + + for n := 0; n < COL_NO_LAST; n++ { + val, err := p.ts.GetValue(citer, n) + if err != nil { + return err + } + + if n == COL_NO_NREFS { + err = p.ts.SetValue(iter, n, nrefs) + } else { + err = p.ts.SetValue(iter, n, val) + } + + if err != nil { + return err + } + } + + p.ts.Remove(citer) + return nil + } + + p.ts.Remove(iter) + + if subidx > -1 { + ppath, err := gtk.TreePathNewFromString(ppathstr) + if err != nil { + return err + } + + piter, err := p.ts.GetIter(ppath) + if err != nil { + return err + } + + nrefs, err := p.lsGetInt(piter, COL_NO_NREFS) + if err != nil { + return err + } + + err = p.ts.SetValue(piter, COL_NO_NREFS, nrefs-1) + if err != nil { + return err + } + } + + p.toggleHover() + return nil +} + +func (p *Prompt) addRecentlyRemoved(guid string) { + p.recentLock.Lock() + defer p.recentLock.Unlock() + fmt.Println("RECENTLY REMOVED: ", guid) + p.recentlyRemoved = append(p.recentlyRemoved, guid) +} + +func (p *Prompt) wasRecentlyRemoved(guid string) bool { + p.recentLock.Lock() + defer p.recentLock.Unlock() + + for gind, g := range p.recentlyRemoved { + if g == guid { + p.recentlyRemoved = append(p.recentlyRemoved[:gind], p.recentlyRemoved[gind+1:]...) + return true + } + } + + return false +} + +func (p *Prompt) RemoveRequest(guid string) { + if p.wasRecentlyRemoved(guid) { + fmt.Printf("Entry for %s was recently removed; deleting from cache\n", guid) + return + } + + removed := false + + if p.ts == nil { + return + } + + p.promptLock.Lock() + defer p.promptLock.Unlock() + +remove_outer: + for ridx := 0; ridx < p.ts.IterNChildren(nil); ridx++ { + nchildren := 0 + this_iter, err := p.ts.GetIterFromString(fmt.Sprintf("%d", ridx)) + if err != nil { + log.Println("Strange condition; couldn't get iter of known tree index:", err) + } else { + nchildren = p.ts.IterNChildren(this_iter) + } + + for cidx := 0; cidx < nchildren-1; cidx++ { + sidx := cidx + if cidx == nchildren { + cidx = -1 + } + + rule, _, err := p.getRuleByIdx(ridx, sidx) + if err != nil { + break remove_outer + } else if rule.GUID == guid { + p.removeSelectedRule(ridx, sidx) + removed = true + break + } + } + } + + if !removed { + fmt.Printf("Unexpected condition: SGFW requested prompt removal for non-existent GUID %v\n", guid) + } +} + +func (p *Prompt) RemoveAll() { + p.promptLock.Lock() + defer p.promptLock.Unlock() + p.recentLock.Lock() + defer p.recentLock.Unlock() + + p.recentlyRemoved = p.recentlyRemoved[:0] + + for { + iter, found := p.ts.GetIterFirst() + if iter == nil || found == false { + break + } + pi, _ := p.ts.GetPath(iter) + if pi == nil { + break + } + p.tv.SetCursor(pi, nil, false) + _, idx, subidx, err := p.getSelectedRule() + if err != nil { + break + } + p.removeSelectedRule(idx, subidx) + } +} + +func (p *Prompt) makeDecision(rule string, scope int, guid string) error { + return p.app.Dbus.answerPrompt(uint32(scope), rule, guid) +} + +func (p *Prompt) buttonAction(guid string, rr *sgfw.DbusRule) { + p.promptLock.Lock() + rule, idx, subidx, err := p.getSelectedRule() + if err != nil { + p.promptLock.Unlock() + warnDialog(&p.app.win.Window, "Error occurred processing request: %s", err.Error()) + return + } + + tk := strings.Split(rr.Target, ":") + // Overlay the rules + rule.Scope = int(rr.Mode) + //rule.Path = urule.Path + rule.Port, _ = strconv.Atoi(tk[1]) + rule.Target = tk[0] + rule.Proto = rr.Proto + rule.UID = int(rr.UID) + rule.GID = int(rr.GID) + // rule.Uname = urule.Uname + // rule.Gname = urule.Gname + + fmt.Println("rule = ", rule) + + action := sgfw.RuleActionString[sgfw.RuleAction(rr.Verb)] + rulestr := action + + proto := rule.Proto + if proto == "any" || proto == "" { + proto = "*" + } + rulestr += "|" + proto + ":" + rule.Target + ":" + strconv.Itoa(rule.Port) + rulestr += "|" + sgfw.RuleModeString[sgfw.RuleMode(rule.Scope)] + rulestr += "|" + strconv.Itoa(rule.UID) + ":" + strconv.Itoa(rule.GID) + if rule.Sandbox != "" { + rulestr += "|" + rule.Sandbox + } + fmt.Println("RULESTR = ", rulestr) + p.makeDecision(rulestr, int(rule.Scope), guid) + err = p.removeSelectedRule(idx, subidx) + p.addRecentlyRemoved(guid) + p.promptLock.Unlock() + if err != nil { + warnDialog(&p.app.win.Window, "Error setting new rule: %s", err.Error()) + } +} diff --git a/fw-settings/rule_edit.go b/fw-settings/rule_edit.go index 6269950..19ae9ac 100644 --- a/fw-settings/rule_edit.go +++ b/fw-settings/rule_edit.go @@ -87,7 +87,7 @@ func (re *ruleEdit) validateFields() bool { if !isValidHost(host) { return false } - if !isValidPort(port) { + if !isValidPort(port, "") { return false } return true @@ -113,7 +113,8 @@ func isValidHost(host string) bool { return true } -func isValidPort(port string) bool { +func isValidPort(port, proto string) bool { + min := 0 if port == "*" { return true } @@ -122,7 +123,11 @@ func isValidPort(port string) bool { if err != nil { return false } - return pval > 0 && pval <= 0xFFFF + + if proto == "icmp" { + min = -1 + } + return pval > min && pval <= 0xFFFF } func (re *ruleEdit) updateRow() { @@ -145,15 +150,15 @@ func (re *ruleEdit) updateRow() { } func (re *ruleEdit) run(saveasnew bool) { - re.dialog.SetTransientFor(re.row.rl.win) + re.dialog.SetTransientFor(&re.row.rl.app.win.Window) if re.dialog.Run() == editDialogOk { if saveasnew { re.row.rule.Mode = uint16(sgfw.RULE_MODE_PERMANENT) } re.updateRow() - re.row.rl.dbus.updateRule(re.row.rule) + re.row.rl.app.Dbus.updateRule(re.row.rule) if saveasnew { - re.row.widget.Hide() + re.row.Hide() } } re.dialog.Destroy() diff --git a/fw-settings/rule_new.go b/fw-settings/rule_new.go new file mode 100644 index 0000000..15ad187 --- /dev/null +++ b/fw-settings/rule_new.go @@ -0,0 +1,551 @@ +package main + +import ( + "fmt" + "os" + "strings" + "strconv" + "unicode" + + "github.com/subgraph/fw-daemon/sgfw" + + "github.com/gotk3/gotk3/gtk" +) + +const ( + newDialogCancel = 1 + newDialogOk = 2 + newDialogAllow = 3 +) + +const ( + COLUMN_ID = iota + COLUMN_NAME +) + +type DialogMode uint + +const ( + DIALOG_MODE_NEW DialogMode = iota + DIALOG_MODE_EDIT + DIALOG_MODE_SAVEAS + DIALOG_MODE_PROMPT + DIALOG_MODE_INFO +) + +const ( + Setuid uint32 = 1 << (12 - 1 - iota) + Setgid + Sticky + UserRead + UserWrite + UserExecute + GroupRead + GroupWrite + GroupExecute + OtherRead + OtherWrite + OtherExecute +) + +type ruleNew struct { + dialog *gtk.Dialog + row *ruleRow + mode DialogMode + nbSelected int + comboUID *gtk.ComboBoxText + checkUID *gtk.CheckButton + comboGID *gtk.ComboBoxText + checkGID *gtk.CheckButton + titleScope *gtk.Label + comboScope *gtk.ComboBoxText + labelScope *gtk.Label + comboVerb *gtk.ComboBoxText + checkTLS *gtk.CheckButton + titleSandbox *gtk.Label + labelSandbox *gtk.Label + comboSandbox *gtk.ComboBoxText + btnPathChooser *gtk.FileChooserButton + entryPath *gtk.Entry + hostEntry *gtk.Entry + portEntry *gtk.Entry + titlePort *gtk.Label + comboProto *gtk.ComboBoxText + ok *gtk.Button + allow *gtk.Button + cancel *gtk.Button + labelPID *gtk.Label + titlePID *gtk.Label + entryOrigin *gtk.Entry + labelOrigin *gtk.Label +} + +func newRuleAdd(rr *ruleRow, mode DialogMode) *ruleNew{ + rnew := &ruleNew{} + rnew.mode = mode + rnew.nbSelected = rr.rl.app.nbRules.GetCurrentPage() + b := newBuilder("RuleNew") + b.getItems( + "dialog", &rnew.dialog, + "uid_combo", &rnew.comboUID, + "uid_checkbox", &rnew.checkUID, + "gid_combo", &rnew.comboGID, + "gid_checkbox", &rnew.checkGID, + "scope_title", &rnew.titleScope, + "scope_combo", &rnew.comboScope, + "scope_label", &rnew.labelScope, + "verb_combo", &rnew.comboVerb, + "tls_check", &rnew.checkTLS, + "sandbox_title", &rnew.titleSandbox, + "sandbox_combo", &rnew.comboSandbox, + "sandbox_label", &rnew.labelSandbox, + "path_chooser", &rnew.btnPathChooser, + "path_entry", &rnew.entryPath, + "host_entry", &rnew.hostEntry, + "port_entry", &rnew.portEntry, + "port_title", &rnew.titlePort, + "proto_combo", &rnew.comboProto, + "ok_button", &rnew.ok, + "allow_button", &rnew.allow, + "cancel_button", &rnew.cancel, + "pid_label", &rnew.labelPID, + "pid_title", &rnew.titlePID, + "origin_entry", &rnew.entryOrigin, + "origin_label", &rnew.labelOrigin, + ) + + b.ConnectSignals(map[string]interface{}{ + "on_proto_changed": rnew.onProtoChanged, + "on_verb_changed": rnew.onVerbChanged, + "on_port_insert_text": rnew.onPortInsertText, + "on_port_changed": rnew.onChanged, + "on_host_changed": rnew.onChanged, + "on_path_changed": rnew.onChanged, + "on_path_set": rnew.onPathSet, + }) + + rnew.row = rr + switch rnew.mode { + case DIALOG_MODE_EDIT: + rnew.dialog.SetTitle("Edit Rule") + case DIALOG_MODE_NEW: + rnew.dialog.SetTitle("Add New Rule") + case DIALOG_MODE_SAVEAS: + rnew.ok.SetLabel("Save As New") + rnew.dialog.SetTitle("Save As New Rule") + case DIALOG_MODE_PROMPT: + rnew.connectShortcutsPromptWindow() + rnew.dialog.SetTitle("Firewall Prompt") + case DIALOG_MODE_INFO: + rnew.cancel.SetLabel("Close") + rnew.dialog.SetTitle("Rule Information") + } + + return rnew +} + +func (re *ruleNew) connectShortcutsPromptWindow() { + app := re.row.rl.app + // Shortcuts Help Registered in Prompt + app.ConnectShortcut("h", "", "", re.dialog.Window, func(win gtk.Window) {re.hostEntry.Widget.GrabFocus()}) + app.ConnectShortcut("p", "", "", re.dialog.Window, func(win gtk.Window) {re.portEntry.Widget.GrabFocus()}) + app.ConnectShortcut("o", "", "", re.dialog.Window, func(win gtk.Window) {re.comboProto.ComboBox.Popup()}) + app.ConnectShortcut("t", "", "", re.dialog.Window, func(win gtk.Window) { + if re.checkTLS.GetSensitive() { + re.checkTLS.SetActive(!re.checkTLS.GetActive()) + } + }) + app.ConnectShortcut("s", "", "", re.dialog.Window, func(win gtk.Window) {re.comboScope.ComboBox.Popup()}) + app.ConnectShortcut("u", "", "", re.dialog.Window, func(win gtk.Window) {re.checkUID.SetActive(!re.checkUID.GetActive())}) + app.ConnectShortcut("g", "", "", re.dialog.Window, func(win gtk.Window) {re.checkGID.SetActive(!re.checkGID.GetActive())}) +} + +func (re *ruleNew) updateRow(res int) { + if !re.validateFields() { + return + } + r := re.row.rule + if re.mode == DIALOG_MODE_PROMPT { + if res == newDialogOk { + r.Verb = uint16(sgfw.RULE_ACTION_DENY) + } else if res == newDialogAllow { + r.Verb = uint16(sgfw.RULE_ACTION_ALLOW) + } + mid, _ := strconv.Atoi(re.comboScope.GetActiveID()) + r.Mode = uint16(mid) + } else { + switch re.comboVerb.GetActiveID() { + case "allow": + r.Verb = uint16(sgfw.RULE_ACTION_ALLOW) + // case "allow_tls": + // r.Verb = uint16(sgfw.RULE_ACTION_ALLOW_TLSONLY) + case "deny": + r.Verb = uint16(sgfw.RULE_ACTION_DENY) + } + } + + r.Proto = re.comboProto.GetActiveID() + if r.Proto == "any" { + r.Proto = "*" + } + if r.Proto == "tcp" && r.Verb == uint16(sgfw.RULE_ACTION_ALLOW) && re.checkTLS.GetActive() { + r.Verb = uint16(sgfw.RULE_ACTION_ALLOW_TLSONLY) + } + + host, _ := re.hostEntry.GetText() + port, _ := re.portEntry.GetText() + r.Target = fmt.Sprintf("%s:%s", host, port) + if re.mode != DIALOG_MODE_PROMPT || re.checkUID.GetActive() == true { + uid, _ := strconv.ParseInt(re.comboUID.GetActiveID(), 10, 32) + r.UID = int32(uid) + } else { + r.UID = -1 + } + if re.mode != DIALOG_MODE_PROMPT || re.checkGID.GetActive() == true { + gid, _ := strconv.ParseInt(re.comboGID.GetActiveID(), 10, 32) + r.GID = int32(gid) + } else { + r.GID = -1 + } + + if re.mode == DIALOG_MODE_NEW { + r.Path = re.btnPathChooser.FileChooser.GetFilename() + mid, _ := strconv.Atoi(re.comboScope.GetActiveID()) + r.Mode = uint16(mid) + r.Sandbox = re.comboSandbox.GetActiveID() + } + + if re.mode != DIALOG_MODE_NEW && re.mode != DIALOG_MODE_PROMPT { + re.row.update() + } +} + +type cbPromptRequest func(guid string, rule *sgfw.DbusRule) + +func (re *ruleNew) run(guid string, cb cbPromptRequest) { + re.dialog.SetTransientFor(re.row.rl.app.win) + re.dialog.ShowAll() + if re.mode == DIALOG_MODE_INFO { + re.dialog.Run() + } else if re.mode == DIALOG_MODE_PROMPT { + res := re.dialog.Run() + if res != newDialogCancel { + re.updateRow(res) + cb(guid, re.row.rule) + } + } else if re.mode == DIALOG_MODE_NEW { + if re.dialog.Run() == newDialogOk { + re.updateRow(newDialogOk) + r := *re.row.rule + res, err := re.row.rl.app.Dbus.addRule(&r) + if res == false || err != nil { + warnDialog(&re.row.rl.app.win.Window, "Error notifying SGFW of asynchronous rule addition:", err) + return + } + } + } else if re.mode == DIALOG_MODE_SAVEAS { + if re.dialog.Run() == newDialogOk { + re.updateRow(newDialogOk) + r := *re.row.rule + re.row.rl.app.Dbus.addRule(&r) + re.row.rl.remove(re.row) + } + } else { + if re.dialog.Run() == newDialogOk { + re.updateRow(newDialogOk) + re.row.rl.app.Dbus.updateRule(re.row.rule) + } + } + re.dialog.Destroy() +} + +func (rr *ruleRow) runNewEditor(mode DialogMode) { + redit := newRuleAdd(rr, mode) + redit.update() + redit.run("", nil) +} + +func (re *ruleNew) update() { + re.populateUID() + re.populateGID() + r := re.row.rule + + if re.mode != DIALOG_MODE_INFO { + re.comboScope.Remove(4) + } + + if re.mode != DIALOG_MODE_PROMPT && re.mode != DIALOG_MODE_INFO { + re.comboScope.Remove(3) + re.comboScope.Remove(2) + } + + re.onVerbChanged() + + if re.mode == DIALOG_MODE_NEW { + if re.nbSelected < 2 { + re.comboScope.SetActive(re.nbSelected) + } else { + re.comboScope.SetActive(0) + } + //re.titleSandbox.SetNoShowAll(true) + //re.titleSandbox.SetVisible(false) + //re.comboSandbox.SetNoShowAll(true) + //re.comboSandbox.SetVisible(false) + //re.comboSandbox.SetNoShowAll(true) + //re.comboSandbox.SetVisible(false) + re.comboSandbox.Append("", "") + for _, pn := range re.row.rl.app.ozProfiles { + re.comboSandbox.Append(pn, pn) + } + re.comboSandbox.SetActive(0) + re.btnPathChooser.SetCurrentFolder("/") + re.ok.SetSensitive(false) + re.onProtoChanged() + + return + } + + if r.Proto == "" { + re.comboProto.SetActiveID("any") + } else { + re.comboProto.SetActiveID(strings.ToLower(r.Proto)) + } + + re.comboSandbox.SetVisible(false) + re.comboSandbox.SetSensitive(false) + re.comboSandbox.SetNoShowAll(true) + + if sgfw.RuleAction(r.Verb) == sgfw.RULE_ACTION_ALLOW || sgfw.RuleAction(r.Verb) == sgfw.RULE_ACTION_ALLOW_TLSONLY { + re.comboVerb.SetActiveID("allow") + } else { + re.comboVerb.SetActiveID("deny") + } + + if sgfw.RuleAction(r.Verb) == sgfw.RULE_ACTION_ALLOW_TLSONLY { + re.checkTLS.SetActive(true) + } + + if r.Sandbox == "" { + re.titleSandbox.SetNoShowAll(true) + re.titleSandbox.SetVisible(false) + re.labelSandbox.SetNoShowAll(true) + re.labelSandbox.SetVisible(false) + } else { + re.titleSandbox.SetVisible(true) + re.labelSandbox.SetNoShowAll(false) + re.labelSandbox.SetVisible(true) + re.labelSandbox.SetNoShowAll(false) + re.labelSandbox.SetText(r.Sandbox) + } + + re.btnPathChooser.SetNoShowAll(true) + re.btnPathChooser.SetVisible(false) + re.btnPathChooser.SetSensitive(false) + re.entryPath.SetNoShowAll(false) + re.entryPath.SetVisible(true) + re.entryPath.SetText(r.Path) + + target := strings.Split(r.Target, ":") + if len(target) != 2 { + return + } + re.hostEntry.SetText(target[0]) + re.portEntry.SetText(target[1]) + + if r.UID > -1 { + re.comboUID.SetActiveID(strconv.FormatInt(int64(r.UID), 10)) + } + if r.GID > -1 { + re.comboGID.SetActiveID(strconv.FormatInt(int64(r.GID), 10)) + } + + if re.mode == DIALOG_MODE_EDIT { + re.comboScope.SetVisible(false) + re.comboScope.SetNoShowAll(true) + re.comboScope.SetSensitive(false) + re.labelScope.SetNoShowAll(false) + re.labelScope.SetVisible(true) + re.labelScope.SetText(strings.Title(strings.ToLower(sgfw.RuleModeString[sgfw.RuleMode(r.Mode)]))) + } + if re.mode == DIALOG_MODE_PROMPT || r.Mode == uint16(sgfw.RULE_MODE_PROCESS) { + re.titlePID.SetNoShowAll(false) + re.titlePID.SetVisible(true) + re.labelPID.SetNoShowAll(false) + re.labelPID.SetVisible(true) + pid := strconv.FormatUint(uint64(r.Pid), 10) + re.labelPID.SetText(pid) + } + if re.mode == DIALOG_MODE_SAVEAS { + re.comboScope.Remove(1) + re.comboScope.SetSensitive(false) + } + if re.mode == DIALOG_MODE_PROMPT { + re.entryOrigin.SetNoShowAll(false) + re.entryOrigin.SetVisible(true) + re.entryOrigin.SetSensitive(false) + re.entryOrigin.SetText(r.Origin) + re.labelOrigin.SetNoShowAll(false) + re.labelOrigin.SetVisible(true) + re.comboUID.SetSensitive(false) + re.comboGID.SetSensitive(false) + re.comboScope.SetActiveID(strconv.Itoa(int(sgfw.RuleModeValue[strings.ToUpper(re.row.rl.app.Config.DefaultAction)]))) + + re.checkUID.SetNoShowAll(false) + re.checkUID.SetVisible(true) + re.checkUID.SetSensitive(true) + re.checkGID.SetNoShowAll(false) + re.checkGID.SetVisible(true) + re.checkGID.SetSensitive(true) + + re.comboVerb.SetNoShowAll(true) + re.comboVerb.SetVisible(false) + re.comboVerb.SetSensitive(false) + + re.setPromptButtons() + + ctv := r.IsSocks + if !ctv { + re.checkTLS.SetSensitive(false) + re.checkTLS.SetActive(false) + } + + } + + if re.mode == DIALOG_MODE_INFO { + re.comboScope.SetActiveID(strconv.Itoa(int(r.Mode))) + re.comboScope.SetSensitive(false) + re.comboVerb.SetSensitive(false) + re.hostEntry.SetSensitive(false) + re.portEntry.SetSensitive(false) + re.comboUID.SetSensitive(false) + re.comboGID.SetSensitive(false) + re.checkUID.SetSensitive(false) + re.checkGID.SetSensitive(false) + re.comboProto.SetSensitive(false) + re.checkTLS.SetSensitive(false) + re.ok.SetNoShowAll(true) + re.ok.SetSensitive(false) + re.ok.SetVisible(false) + } + + re.onProtoChanged() +} + +func (re *ruleNew) setPromptButtons() { + re.allow.SetNoShowAll(false) + re.allow.SetVisible(true) + re.allow.SetSensitive(true) + re.ok.SetLabel("_Deny") +} + +func (re *ruleNew) toggleCheckTLS(val bool) { + if val && re.row.rule.IsSocks && re.mode != DIALOG_MODE_NEW && re.mode != DIALOG_MODE_INFO { + re.checkTLS.SetSensitive(true) + } else { + re.checkTLS.SetSensitive(false) + } +} + +func (re *ruleNew) onProtoChanged() { + re.toggleCheckTLS( (re.comboProto.GetActiveID() == "tcp") ) + if re.comboProto.GetActiveID() == "icmp" { + re.titlePort.SetText("Code:") + re.portEntry.SetPlaceholderText("Code") + } else { + re.titlePort.SetText("Port:") + re.portEntry.SetPlaceholderText("Port") + } + re.onChanged() +} + +func (re *ruleNew) onVerbChanged() { + re.toggleCheckTLS( (re.comboVerb.GetActiveID() == "allow") ) +} + +func (re *ruleNew) validateFields() bool { + id := re.comboVerb.GetActiveID() + if id != "allow" && id != "allow_tls" && id != "deny" { + return false + } + proto := re.comboProto.GetActiveID() + protos := []string{"", "tcp", "udp", "icmp"} + found := false + for _, p := range protos { + if proto == p { + found = true + break + } + } + if !found { + return false + } + host, _ := re.hostEntry.GetText() + port, _ := re.portEntry.GetText() + if !isValidHost(host) { + return false + } + if !isValidPort(port, re.comboProto.GetActiveID()) { + return false + } + if re.mode == DIALOG_MODE_NEW { + fp := re.btnPathChooser.FileChooser.GetFilename() + if fp == "" || !isExecutableFile(fp) { + return false + } + } + return true +} + +func isExecutableFile(file string) bool { + fi, _ := os.Stat(file) + fm := fi.Mode() + perm := uint32(fm.Perm()) + return !( (perm&UserExecute == 0) && (perm&GroupExecute == 0) && (perm&OtherExecute == 0) ) + +} + +func (re *ruleNew) 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 *ruleNew) onChanged() { + valid := re.validateFields() + re.ok.SetSensitive(valid) + if re.mode == DIALOG_MODE_PROMPT { + re.allow.SetSensitive(valid) + } +} + +func (re *ruleNew) onPathSet(btnChooser *gtk.FileChooserButton) { + fp := btnChooser.FileChooser.GetFilename() + if !isExecutableFile(fp) { + warnDialog(&re.row.rl.app.win.Window, "%s", "File not an executable!") + } else { + btnChooser.SetTooltipText(fp) + } +} + +func (re *ruleNew) populateUID() { + for _, id := range re.row.rl.app.userIDs { + re.comboUID.Append(strconv.FormatInt(int64(id), 10), re.row.rl.app.userMap[id]) + } +} + +func (re *ruleNew) populateGID() { + for _, id := range re.row.rl.app.groupIDs { + re.comboGID.Append(strconv.FormatInt(int64(id), 10), re.row.rl.app.groupMap[id]) + } +} diff --git a/fw-settings/rules.go b/fw-settings/rules.go index bb3d9f1..892b222 100644 --- a/fw-settings/rules.go +++ b/fw-settings/rules.go @@ -3,91 +3,177 @@ package main import ( "fmt" "os" - "strconv" "strings" + "sync" "github.com/subgraph/fw-daemon/sgfw" "github.com/gotk3/gotk3/gtk" + "github.com/gotk3/gotk3/glib" ) type ruleList struct { - dbus *dbusObject - win *gtk.Window - list *gtk.ListBox - col1 *gtk.SizeGroup - col2 *gtk.SizeGroup - col3 *gtk.SizeGroup - col4 *gtk.SizeGroup - col5 *gtk.SizeGroup + lock *sync.Mutex + app *fwApp + mode sgfw.RuleMode + rows []*ruleRow + rules []sgfw.DbusRule + rowsByIndex map[int]*ruleRow + list *gtk.ListBox + col0 *gtk.SizeGroup + col1 *gtk.SizeGroup + col2 *gtk.SizeGroup + col3 *gtk.SizeGroup + raHandlerID glib.SignalHandle } type ruleRow struct { + *gtk.ListBoxRow rl *ruleList rule *sgfw.DbusRule - widget *gtk.ListBoxRow + gtkBox *gtk.Box + gtkSep *gtk.Separator + gtkGrid *gtk.Grid gtkLabelApp *gtk.Label - gtkLabelVerb *gtk.Label - gtkLabelOrigin *gtk.Label - gtkLabelPrivs *gtk.Label gtkLabelTarget *gtk.Label gtkButtonEdit *gtk.Button gtkButtonSave *gtk.Button gtkButtonDelete *gtk.Button + gtkAppIcon *gtk.Image + gtkIconVerb *gtk.Image } -func newRuleList(dbus *dbusObject, win *gtk.Window, list *gtk.ListBox) *ruleList { - rl := &ruleList{dbus: dbus, win: win, list: list} +func newRuleList(app *fwApp, list *gtk.ListBox, mode sgfw.RuleMode) *ruleList { + rl := &ruleList{app: app, list: list} + rl.lock = new(sync.Mutex) + rl.mode = mode rl.list.SetSelectionMode(gtk.SELECTION_NONE) + rl.col0, _ = gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL) rl.col1, _ = gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL) rl.col2, _ = gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL) rl.col3, _ = gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL) - rl.col4, _ = gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL) - rl.col5, _ = gtk.SizeGroupNew(gtk.SIZE_GROUP_HORIZONTAL) + rl.list.SetActivateOnSingleClick(false) return rl } -func (rl *ruleList) loadRules(mode sgfw.RuleMode) error { - rules, err := rl.dbus.listRules() +func (rl *ruleList) loadRules(noAdd bool) error { + rl.lock.Lock() + defer rl.lock.Unlock() + rules, err := rl.app.Dbus.listRules() if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %+v\n", err) return err } - rl.addRules(rules, mode) + + for i := (len(rules) - 1); i >= 0; i-- { + if sgfw.RuleMode(rules[i].Mode) != rl.mode { + rules = append(rules[:i], rules[i+1:]...) + } + } + rules = rl.sortRules(rules) + rl.rules = rules + if !noAdd { + rl.addRules(rules) + } return nil } -func (rl *ruleList) addRules(rules []sgfw.DbusRule, mode sgfw.RuleMode) { - for i := 0; i < len(rules); i++ { - if sgfw.RuleMode(rules[i].Mode) != mode { - continue +func (rl *ruleList) reloadRules(filter string) { + rl.lock.Lock() + defer rl.lock.Unlock() + filter = strings.ToLower(filter) + rules := make([]sgfw.DbusRule, len(rl.rules)) + copy(rules, rl.rules) + if filter != "" { + for i := (len(rules) - 1); i >= 0; i-- { + if !strings.Contains(strings.ToLower(rules[i].Path), filter) && !strings.Contains(strings.ToLower(rules[i].Sandbox), filter) { + rules = append(rules[:i], rules[i+1:]...) + } } - row := createWidget(&rules[i]) - row.rl = rl + } + rules = rl.sortRules(rules) + + for i, _ := range rl.rows { + rl.col0.RemoveWidget(rl.rows[i].gtkAppIcon) + rl.col1.RemoveWidget(rl.rows[i].gtkLabelApp) + rl.col2.RemoveWidget(rl.rows[i].gtkIconVerb) + rl.col3.RemoveWidget(rl.rows[i].gtkLabelTarget) + + rl.rows[i].gtkLabelApp.Destroy() + rl.rows[i].gtkLabelApp = nil + rl.rows[i].gtkLabelTarget.Destroy() + rl.rows[i].gtkLabelTarget = nil + rl.rows[i].gtkButtonEdit.Destroy() + rl.rows[i].gtkButtonEdit = nil + rl.rows[i].gtkButtonSave.Destroy() + rl.rows[i].gtkButtonSave = nil + rl.rows[i].gtkButtonDelete.Destroy() + rl.rows[i].gtkButtonDelete = nil + rl.rows[i].gtkAppIcon.Destroy() + rl.rows[i].gtkAppIcon = nil + rl.rows[i].gtkIconVerb.Destroy() + rl.rows[i].gtkIconVerb = nil + + rl.rows[i].gtkGrid.Destroy() + rl.rows[i].gtkGrid = nil + rl.rows[i].gtkSep.Destroy() + rl.rows[i].gtkSep = nil + rl.rows[i].gtkBox.Destroy() + rl.rows[i].gtkBox = nil + + rl.list.Remove(rl.rows[i]) + rl.rows[i].ListBoxRow.Destroy() + rl.rows[i].ListBoxRow = nil + //rl.rows[i].Destroy() + rl.rows[i].rule = nil + rl.rows[i].rl = nil + rl.rows[i] = nil + } + rl.rows = rl.rows[:0] + for i, _ := range rl.rowsByIndex { + delete(rl.rowsByIndex, i) + } + rules = rl.sortRules(rules) + rl.addRules(rules) +} + +func (rl *ruleList) addRules(rules []sgfw.DbusRule) { + pi := 0 + rl.rowsByIndex = make(map[int]*ruleRow, len(rules)) + if rl.raHandlerID > 0 { + rl.list.HandlerDisconnect(rl.raHandlerID) + } + for i := 0; i < len(rules); i++ { + row := rl.createWidget(&rules[i]) + rl.col0.AddWidget(row.gtkAppIcon) rl.col1.AddWidget(row.gtkLabelApp) - rl.col2.AddWidget(row.gtkLabelVerb) - rl.col3.AddWidget(row.gtkLabelOrigin) - rl.col4.AddWidget(row.gtkLabelPrivs) - rl.col5.AddWidget(row.gtkLabelTarget) - rl.list.Add(row.widget) + rl.col2.AddWidget(row.gtkIconVerb) + rl.col3.AddWidget(row.gtkLabelTarget) + rl.list.Add(row) + rl.rowsByIndex[row.GetIndex()] = row + row.ShowAll() + if i > 0 && rules[pi].Path == rules[i].Path && rules[pi].Sandbox == rules[i].Sandbox { + row.hideTitle() + } + rl.rows = append(rl.rows, row) + pi = i } + rl.raHandlerID, _ = rl.list.Connect("row-activated", rl.showInformation) } -func createWidget(rule *sgfw.DbusRule) *ruleRow { - row := &ruleRow{} +func (rl *ruleList) createWidget(rule *sgfw.DbusRule) *ruleRow { + row := &ruleRow{rl: rl} row.rule = rule builder := newBuilder("RuleItem") - var grid *gtk.Grid builder.getItems( - "grid", &grid, + "grid", &row.gtkGrid, "app_label", &row.gtkLabelApp, - "verb_label", &row.gtkLabelVerb, - "origin_label", &row.gtkLabelOrigin, - "privs_label", &row.gtkLabelPrivs, + "verb_icon", &row.gtkIconVerb, "target_label", &row.gtkLabelTarget, "edit_button", &row.gtkButtonEdit, "save_button", &row.gtkButtonSave, "delete_button", &row.gtkButtonDelete, + "app_icon", &row.gtkAppIcon, ) switch sgfw.RuleMode(rule.Mode) { case sgfw.RULE_MODE_SYSTEM: @@ -96,6 +182,10 @@ func createWidget(rule *sgfw.DbusRule) *ruleRow { row.gtkButtonDelete.SetSensitive(false) row.gtkButtonDelete.SetTooltipText("Cannot delete system rules") break + case sgfw.RULE_MODE_PROCESS: + row.gtkButtonSave.SetSensitive(true) + row.gtkButtonSave.SetNoShowAll(false) + break case sgfw.RULE_MODE_SESSION: row.gtkButtonSave.SetSensitive(true) row.gtkButtonSave.SetNoShowAll(false) @@ -107,28 +197,104 @@ func createWidget(rule *sgfw.DbusRule) *ruleRow { "on_save_rule": row.onSaveAsNew, "on_delete_rule": row.onDelete, }) - row.widget, _ = gtk.ListBoxRowNew() - row.widget.Add(grid) + row.gtkBox, _ = gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0) + row.gtkSep, _ = gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL) + row.ListBoxRow, _ = gtk.ListBoxRowNew() + row.gtkBox.Add(row.gtkGrid) + row.gtkBox.Add(row.gtkSep) + row.Add(row.gtkBox) + row.SetProperty("selectable", false) + row.SetProperty("activatable", true) + row.showTitle() row.update() + //builder.Object.Unref() + builder = nil return row } +func (rl *ruleList) showInformation(list *gtk.ListBox, row *gtk.ListBoxRow) bool { + rr := rl.rowsByIndex[row.GetIndex()] + rr.runNewEditor(DIALOG_MODE_INFO) + return true +} + func (rr *ruleRow) update() { - if rr.rule.Mode == uint16(sgfw.RULE_MODE_PROCESS) { - appstr := "(" + strconv.Itoa(int(rr.rule.Pid)) + ") " + rr.rule.App - rr.gtkLabelApp.SetText(appstr) - } else { - rr.gtkLabelApp.SetText(rr.rule.App) - } rr.gtkLabelApp.SetTooltipText(rr.rule.Path) - rr.gtkLabelVerb.SetText(getVerbText(rr.rule)) - if rr.rule.Proto == "tcp" { - rr.gtkLabelOrigin.SetText(rr.rule.Origin) + rr.setVerbIcon() + tt := getTargetText(rr.rule) + if rr.rule.UID > -1 || rr.rule.GID > -1 { + tt = tt + " for " + } + if rr.rule.UID > -1 { + tt = tt + rr.rl.app.LookupUsername(rr.rule.UID) + } + if rr.rule.UID > -1 && rr.rule.GID > -1 { + tt = tt + ":" + } + if rr.rule.GID > -1 { + tt = tt + rr.rl.app.LookupGroup(rr.rule.GID) + } + rr.gtkLabelTarget.SetText(tt) +} + +func (rr *ruleRow) hideTitle() { + rr.gtkLabelApp.SetText("") + rr.gtkAppIcon.Clear() +} + +func (rr *ruleRow) showTitle() { + in := []string{rr.rule.App} + if rr.rule.Sandbox != "" { + in = append([]string{rr.rule.Sandbox}, in...) + } + if rr.rule.App == "[unknown]" { + in = []string{"image-missing"} + } + it, err := gtk.IconThemeGetDefault() + if err != nil { + fmt.Println("Error getting icon theme.") } else { - rr.gtkLabelOrigin.SetText(rr.rule.Origin + " (" + rr.rule.Proto + ")") + found := false + for _, ia := range in { + pb, _ := it.LoadIcon(ia, int(gtk.ICON_SIZE_BUTTON), gtk.ICON_LOOKUP_USE_BUILTIN) + if pb != nil { + rr.gtkAppIcon.SetFromIconName(ia, gtk.ICON_SIZE_BUTTON) + found = true + break + } + } + if !found { + rr.gtkAppIcon.SetFromIconName("terminal", gtk.ICON_SIZE_BUTTON) + } + } + rr.gtkLabelApp.SetText(rr.rule.App) +} + +func (rr *ruleRow) setVerbIcon() { + it, err := gtk.IconThemeGetDefault() + in := "" + tt := "" + if sgfw.RuleAction(rr.rule.Verb) == sgfw.RULE_ACTION_DENY { + in = "gtk-no" + tt = "Deny" + } else if sgfw.RuleAction(rr.rule.Verb) == sgfw.RULE_ACTION_ALLOW { + in = "gtk-yes" + tt = "Allow" + } else if sgfw.RuleAction(rr.rule.Verb) == sgfw.RULE_ACTION_ALLOW_TLSONLY { + in = "gtk-yes" + tt = "Allow TLS" + } + if err != nil { + fmt.Println("Error getting icon theme.") + return } - rr.gtkLabelPrivs.SetText(rr.rule.Privs) - rr.gtkLabelTarget.SetText(getTargetText(rr.rule)) + pb, _ := it.LoadIcon(in, int(gtk.ICON_SIZE_BUTTON), gtk.ICON_LOOKUP_USE_BUILTIN) + if pb == nil { + fmt.Println("Error getting icon theme.") + return + } + rr.gtkIconVerb.SetFromIconName(in, gtk.ICON_SIZE_BUTTON) + rr.gtkIconVerb.SetTooltipText(tt) } func getVerbText(rule *sgfw.DbusRule) string { @@ -142,44 +308,73 @@ func getVerbText(rule *sgfw.DbusRule) string { } func getTargetText(rule *sgfw.DbusRule) string { + verb := "Deny" + if sgfw.RuleAction(rule.Verb) == sgfw.RULE_ACTION_ALLOW || sgfw.RuleAction(rule.Verb) == sgfw.RULE_ACTION_ALLOW_TLSONLY { + verb = "Allow" + } if rule.Target == "*:*" { - return "All connections" + ct := "any" + if sgfw.RuleAction(rule.Verb) == sgfw.RULE_ACTION_DENY { + ct = "all" + } + res := []string{verb, ct, "connections"} + if sgfw.RuleAction(rule.Verb) == sgfw.RULE_ACTION_ALLOW_TLSONLY { + res = append(res, "with TLS") + } + return strings.Join(res, " ") } - items := strings.Split(rule.Target, ":") + items := strings.Split(rule.Target, ":") if len(items) != 2 { - return rule.Target + return strings.Join([]string{verb, rule.Target}, " ") } + ct := "connections" + if rule.Proto != "tcp" { + ct = "data" + } + target := []string{verb, strings.ToUpper(rule.Proto), ct} + if sgfw.RuleAction(rule.Verb) == sgfw.RULE_ACTION_ALLOW_TLSONLY { + target = append(target, "with TLS") + } + if rule.Origin != "" { + target = append(target, "from ", rule.Origin) + } if items[0] == "*" { if rule.Proto == "tcp" { - return fmt.Sprintf("Connections to ALL hosts on port %s", items[1]) + target = append(target, fmt.Sprintf("to ALL hosts on port %s", items[1])) } else if rule.Proto == "icmp" { - return fmt.Sprintf("Data to ALL hosts with ICMP code %s", items[1]) + target = append(target, fmt.Sprintf("to ALL hosts with code %s", items[1])) + } else { + target = append(target, fmt.Sprintf("to ALL hosts on port %s", items[1])) } - return fmt.Sprintf("Data to ALL hosts on port %s", items[1]) + return strings.Join(target, " ") } if items[1] == "*" { if rule.Proto == "tcp" { - return fmt.Sprintf("All connections to host %s", items[0]) + target = append(target, fmt.Sprintf("to host %s", items[0])) + } else if rule.Proto == "icmp" { + target = append(target, fmt.Sprintf("to host %s", items[0])) + } else { + target = append(target, fmt.Sprintf("to host %s", items[0])) } - return fmt.Sprintf("All data to host %s", items[0]) + return strings.Join(target, " ") } - - if rule.Proto == "tcp" { - return fmt.Sprintf("Connections to %s on port %s", items[0], items[1]) - } else if rule.Proto == "icmp" { - return fmt.Sprintf("Data to %s with ICMP code %s", items[0], items[1]) + ps := "port" + if rule.Proto == "icmp" { + ps = "code" } - return fmt.Sprintf("Data to %s on port %s", items[0], items[1]) + target = append(target, fmt.Sprintf("to %s on %s %s", items[0], ps, items[1])) + + return strings.Join(target, " ") } func (rr *ruleRow) onSaveAsNew() { - rr.runEditor(true) + rr.runNewEditor(DIALOG_MODE_SAVEAS) } func (rr *ruleRow) onEdit() { - rr.runEditor(false) + rr.runNewEditor(DIALOG_MODE_EDIT) } func (rr *ruleRow) onDelete() { @@ -187,22 +382,22 @@ func (rr *ruleRow) onDelete() { if rr.rule.Sandbox != "" { ss := `Are you sure you want to delete this rule: - Path: %s +Path: %s - Sandbox: %s +Sandbox: %s - Rule: %s %s` - body = fmt.Sprintf(ss, rr.rule.Path, rr.rule.Sandbox, getVerbText(rr.rule), getTargetText(rr.rule)) +Rule: %s` + body = fmt.Sprintf(ss, rr.rule.Path, rr.rule.Sandbox, getTargetText(rr.rule)) } else { ss := `Are you sure you want to delete this rule: - Path: %s +Path: %s - Rule: %s %s` - body = fmt.Sprintf(ss, rr.rule.Path, getVerbText(rr.rule), getTargetText(rr.rule)) +Rule: %s` + body = fmt.Sprintf(ss, rr.rule.Path, getTargetText(rr.rule)) } d := gtk.MessageDialogNewWithMarkup( - rr.rl.win, + rr.rl.app.win, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_OK_CANCEL, @@ -212,19 +407,37 @@ func (rr *ruleRow) onDelete() { rr.delete() } d.Destroy() - } func (rl *ruleList) remove(rr *ruleRow) { + rl.col0.RemoveWidget(rr.gtkAppIcon) rl.col1.RemoveWidget(rr.gtkLabelApp) - rl.col2.RemoveWidget(rr.gtkLabelVerb) - rl.col3.RemoveWidget(rr.gtkLabelOrigin) - rl.col4.RemoveWidget(rr.gtkLabelPrivs) - rl.col5.RemoveWidget(rr.gtkLabelTarget) - rl.list.Remove(rr.widget) + rl.col2.RemoveWidget(rr.gtkIconVerb) + rl.col3.RemoveWidget(rr.gtkLabelTarget) + rl.list.Remove(rr.ListBoxRow) + for i := (len(rl.rules) - 1); i >= 0; i-- { + if *rr.rule == rl.rules[i] { + rl.rules = append(rl.rules[:i], rl.rules[i+1:]...) + break; + } + } + } func (rr *ruleRow) delete() { + idx := rr.ListBoxRow.GetIndex() + ndx := idx + 1 + pdx := idx - 1 + if ndx < len(rr.rl.rows) { + if pdx != -1 { + if rr.rl.rows[pdx].rule.Path != rr.rule.Path || rr.rl.rows[pdx].rule.Sandbox != rr.rule.Sandbox { + rr.rl.rows[ndx].showTitle() + } + } else { + rr.rl.rows[ndx].showTitle() + } + } rr.rl.remove(rr) - rr.rl.dbus.deleteRule(rr.rule.ID) + rr.rl.app.Dbus.deleteRule(rr.rule.ID) + rr.rl.rows = append(rr.rl.rows[:idx], rr.rl.rows[idx+1:]...) } diff --git a/fw-settings/settings/definitions/Makefile b/fw-settings/settings/definitions/Makefile new file mode 100644 index 0000000..7309f5c --- /dev/null +++ b/fw-settings/settings/definitions/Makefile @@ -0,0 +1,17 @@ +.PHONY: generate + +all: generate + +gschemas.compiled: *.xml + glib-compile-schemas . + +schemas.go: gschemas.compiled + ruby ./generate.rb + +generate: touch schemas.go + +touch: + ls *.xml | xargs -n1 touch + +doctor: touch generate + git diff --exit-code . diff --git a/fw-settings/settings/definitions/fw-settings.gschema.xml b/fw-settings/settings/definitions/fw-settings.gschema.xml new file mode 100644 index 0000000..6caec0c --- /dev/null +++ b/fw-settings/settings/definitions/fw-settings.gschema.xml @@ -0,0 +1,31 @@ + + + + + + 0 + Main window height + Preserved height of the main window + + + 0 + Main window width + Preserved width of the main window + + + 0 + Main window top + Preserved top coordinate of the main window + + + 0 + Main window left + Preserved left coordinate of the main window + + + true + GTK Prompt As Top Level + Bring the prompt window as front most and sticky on new prompt requests + + + diff --git a/fw-settings/settings/definitions/gen_file.go b/fw-settings/settings/definitions/gen_file.go new file mode 100644 index 0000000..e57cb77 --- /dev/null +++ b/fw-settings/settings/definitions/gen_file.go @@ -0,0 +1,23 @@ +package definitions + +import ( + "encoding/hex" + "io/ioutil" + "path" +) + +func fileContent() []byte { + decoded, _ := hex.DecodeString(schemaDefinition) + return decoded +} + +func writeSchemaToDir(dir string) { + ioutil.WriteFile(path.Join(dir, "gschemas.compiled"), fileContent(), 0664) +} + +// SchemaInTempDir will create a new temporary directory and put the gsettings schema file in there. It is the callers responsibility to remove the directory +func SchemaInTempDir() string { + dir, _ := ioutil.TempDir("", "fw-settings-gschema") + writeSchemaToDir(dir) + return dir +} diff --git a/fw-settings/settings/definitions/generate.rb b/fw-settings/settings/definitions/generate.rb new file mode 100644 index 0000000..cf315d4 --- /dev/null +++ b/fw-settings/settings/definitions/generate.rb @@ -0,0 +1,19 @@ +#!/usr/bin/env ruby + +def gen_go_file + binary_definition = File.binread("gschemas.compiled") + hex = binary_definition.each_byte.map { |b| "%02x" % b }.join + + File.open("schemas.go", "w") do |f| + sliced = hex.chars.each_slice(80).map{ |s| s.join }.join "\"+\n\t\"" + + f.puts <