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()) } }