mirror of https://github.com/subgraph/fw-daemon
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
554 lines
17 KiB
554 lines
17 KiB
const Clutter = imports.gi.Clutter;
|
|
const GLib = imports.gi.GLib;
|
|
const Gtk = imports.gi.Gtk;
|
|
const Lang = imports.lang;
|
|
const Pango = imports.gi.Pango;
|
|
const Signals = imports.signals;
|
|
const St = imports.gi.St;
|
|
|
|
const ModalDialog = imports.ui.modalDialog;
|
|
const Tweener = imports.ui.tweener;
|
|
|
|
const RuleScope = {
|
|
APPLY_ONCE: 0,
|
|
APPLY_SESSION: 1,
|
|
APPLY_PROCESS: 2,
|
|
APPLY_FOREVER: 3,
|
|
};
|
|
|
|
const DetailSection = new Lang.Class({
|
|
Name: 'DetailSection',
|
|
|
|
_init: function() {
|
|
this.actor = new St.BoxLayout({ style_class: 'fw-details-section' });
|
|
this._left = new St.BoxLayout({ vertical: true, style_class: 'fw-details-left'});
|
|
this._right = new St.BoxLayout({ vertical: true });
|
|
this.actor.add_child(this._left);
|
|
this.actor.add_child(this._right);
|
|
|
|
this.ipAddr = this._addDetails("IP Address:");
|
|
this.path = this._addDetails("Path:");
|
|
this.pid = this._addDetails("Process ID:");
|
|
this.origin = this._addDetails("Origin:");
|
|
this.user = this._addDetails("User:");
|
|
this.group = this._addDetails("Group:");
|
|
this.optstring = this._addDetails("");
|
|
},
|
|
|
|
_addDetails: function(text) {
|
|
let title = new St.Label({ style_class: 'fw-detail-title', text: text});
|
|
let msg = new St.Label({ style_class: 'fw-detail-message' });
|
|
this._left.add(title, { expand: true, x_fill: false, x_align: St.Align.END});
|
|
this._right.add(msg);
|
|
return msg;
|
|
},
|
|
|
|
setDetails: function(ip, path, pid, uid, gid, user, group, origin, proto, optstring) {
|
|
this.ipAddr.text = ip;
|
|
this.path.text = path;
|
|
|
|
if (pid == -1) {
|
|
this.pid.text = '[unknown]';
|
|
} else {
|
|
this.pid.text = pid.toString();
|
|
}
|
|
|
|
this.origin.text = origin;
|
|
|
|
if (user != "") {
|
|
this.user.text = user;
|
|
if (uid != -1) {
|
|
this.user.text += " (" + uid.toString() + ")";
|
|
}
|
|
} else {
|
|
this.user.text = "uid:" + uid.toString();
|
|
}
|
|
|
|
if (group != "") {
|
|
this.group.text = group;
|
|
if (gid != -1) {
|
|
this.group.text += " (" + gid.toString() + ")";
|
|
}
|
|
} else {
|
|
this.group.text = "gid:" + gid.toString();
|
|
}
|
|
|
|
this.optstring.text = optstring
|
|
}
|
|
});
|
|
|
|
const OptionListItem = new Lang.Class({
|
|
Name: 'OptionListItem',
|
|
|
|
_init: function(text, idx) {
|
|
this.actor = new St.BoxLayout({ style_class: 'fw-option-item', reactive: true, can_focus: true });
|
|
this._selectedIcon = new St.Icon({style_class: 'fw-option-item-icon', icon_name: 'object-select-symbolic'});
|
|
this._selectedIcon.opacity = 0;
|
|
|
|
this._label = new St.Label({text: text});
|
|
let spacer = new St.Bin();
|
|
this.actor.add_child(this._label);
|
|
this.actor.add(spacer, {expand: true});
|
|
this.actor.add_child(this._selectedIcon);
|
|
this.idx = idx;
|
|
|
|
let action = new Clutter.ClickAction();
|
|
action.connect('clicked', Lang.bind(this, function() {
|
|
this.actor.grab_key_focus();
|
|
this.emit('selected');
|
|
}));
|
|
this.actor.add_action(action);
|
|
|
|
this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
|
|
},
|
|
|
|
setText: function(text) {
|
|
if (text) {
|
|
this._label.text = text;
|
|
this._label.show();
|
|
this.actor.show();
|
|
} else {
|
|
this._label.text = "";
|
|
this._label.hide();
|
|
this.actor.hide();
|
|
}
|
|
},
|
|
|
|
setSelected: function(isSelected) {
|
|
this._selectedIcon.opacity = isSelected ? 255 : 0;
|
|
},
|
|
|
|
_onKeyPressEvent: function(actor, event) {
|
|
let symbol = event.get_key_symbol();
|
|
if (symbol == Clutter.KEY_space || symbol == Clutter.KEY_Return) {
|
|
this.emit('selected');
|
|
}
|
|
}
|
|
});
|
|
Signals.addSignalMethods(OptionListItem.prototype);
|
|
|
|
const OptionList = new Lang.Class({
|
|
Name: 'OptionList',
|
|
|
|
_init: function(pid_known) {
|
|
this.actor = new St.BoxLayout({vertical: true, style_class: 'fw-option-list'});
|
|
if (pid_known) {
|
|
this.buttonGroup = new ButtonGroup("Forever", "Session", "Once", "PID");
|
|
} else {
|
|
this.buttonGroup = new ButtonGroup("Forever", "Session", "Once");
|
|
}
|
|
this.actor.add_child(this.buttonGroup.actor);
|
|
this.items = [];
|
|
this._selected;
|
|
},
|
|
|
|
setOptionText: function(idx, text) {
|
|
if(this.items.length <= idx) {
|
|
log("attempt to setOptionText with idx = "+ idx + " when this.items.length = "+ this.items.length)
|
|
return;
|
|
}
|
|
this.items[idx].setText(text);
|
|
},
|
|
|
|
addOptions: function(options) {
|
|
for(let i = 0; i < options.length; i++) {
|
|
this._addOption(options[i], i)
|
|
}
|
|
if(this.items.length) {
|
|
this._optionSelected(this.items[0])
|
|
}
|
|
},
|
|
|
|
_addOption: function(text, idx) {
|
|
let item = new OptionListItem(text, idx);
|
|
item.connect('selected', Lang.bind(this, function() {
|
|
this._optionSelected(item);
|
|
}));
|
|
this.actor.add_child(item.actor);
|
|
this.items.push(item);
|
|
},
|
|
|
|
_optionSelected: function(item) {
|
|
if (item == this._selected) {
|
|
return;
|
|
}
|
|
if(this._selected) {
|
|
this._selected.actor.remove_style_pseudo_class('selected');
|
|
this._selected.setSelected(false);
|
|
}
|
|
item.setSelected(true);
|
|
this._selected = item;
|
|
this._selected.actor.add_style_pseudo_class('selected');
|
|
},
|
|
|
|
selectedIdx: function() {
|
|
return this._selected.idx;
|
|
},
|
|
|
|
selectedScope: function() {
|
|
switch(this.buttonGroup._checked) {
|
|
case 0:
|
|
return RuleScope.APPLY_FOREVER;
|
|
case 1:
|
|
return RuleScope.APPLY_SESSION;
|
|
case 2:
|
|
return RuleScope.APPLY_ONCE;
|
|
case 3:
|
|
return RuleScope.APPLY_PROCESS;
|
|
default:
|
|
log("unexpected scope value "+ this.buttonGroup._selected);
|
|
return RuleScope.APPLY_SESSION;
|
|
}
|
|
},
|
|
|
|
scopeToIdx: function(scope) {
|
|
switch (scope) {
|
|
case RuleScope.APPLY_PROCESS:
|
|
return 3;
|
|
case RuleScope.APPLY_ONCE:
|
|
return 2;
|
|
case RuleScope.APPLY_SESSION:
|
|
return 1;
|
|
case RuleScope.APPLY_FOREVER:
|
|
return 0;
|
|
default:
|
|
log("unexpected scope value "+ scope);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
const ButtonGroup = new Lang.Class({
|
|
Name: 'ButtonGroup',
|
|
|
|
_init: function() {
|
|
this.actor = new St.BoxLayout({ style_class: 'fw-button-group'});
|
|
this._checked = -1;
|
|
this._buttons= [];
|
|
for(let i = 0; i < arguments.length; i++) {
|
|
let idx = i;
|
|
this._buttons[i] = new St.Button({ style_class: 'fw-group-button button',
|
|
label: arguments[i],
|
|
can_focus: true,
|
|
x_expand: true });
|
|
this._buttons[i].connect('clicked', Lang.bind(this, function(actor) {
|
|
this._setChecked(idx);
|
|
}));
|
|
this.actor.add_child(this._buttons[i]);
|
|
}
|
|
this._setChecked(0);
|
|
},
|
|
|
|
_setChecked: function(idx) {
|
|
|
|
if(idx == this._checked) {
|
|
return;
|
|
}
|
|
this._buttons[idx].add_style_pseudo_class('checked');
|
|
if(this._checked >= 0) {
|
|
this._buttons[this._checked].remove_style_pseudo_class('checked');
|
|
}
|
|
this._checked = idx;
|
|
},
|
|
|
|
});
|
|
|
|
const ExpandingSection = new Lang.Class({
|
|
Name: 'ExpandingSection',
|
|
|
|
_init: function(text, content) {
|
|
this.actor = new St.BoxLayout({vertical: true});
|
|
this._createHeader(this.actor, text);
|
|
this.scroll = new St.ScrollView({hscrollbar_policy: Gtk.PolicyType.NEVER,
|
|
vscrollbar_policy: Gtk.PolicyType.NEVER });
|
|
this.actor.add_child(this.scroll);
|
|
this.isOpen = false;
|
|
},
|
|
|
|
_createHeader: function(parent, text) {
|
|
this.header = new St.BoxLayout({ style_class: 'fw-expanding-section-header', reactive: true, track_hover: true, can_focus: true});
|
|
this.label = new St.Label({ style_class: 'fw-expanding-section-label', text: text, y_expand: true, y_align: Clutter.ActorAlign.CENTER });
|
|
this.header.add_child(this.label);
|
|
let spacer = new St.Bin({ style_class: 'fw-expanding-section-spacer'});
|
|
this.header.add(spacer, {expand: true});
|
|
|
|
this._triangle = new St.Icon({ style_class: 'popup-menu-arrow',
|
|
icon_name: 'pan-end-symbolic',
|
|
y_expand: true,
|
|
y_align: Clutter.ActorAlign.CENTER});
|
|
this._triangle.pivot_point = new Clutter.Point({ x: 0.5, y: 0.6 });
|
|
|
|
this._triangleBin = new St.Widget({ y_expand: true, y_align: Clutter.ActorAlign.CENTER});
|
|
this._triangleBin.add_child(this._triangle);
|
|
|
|
this.header.add_child(this._triangleBin);
|
|
this.header.connect('button-press-event', Lang.bind(this, this._onButtonPressEvent));
|
|
this.header.connect('button-release-event', Lang.bind(this, this._onButtonReleaseEvent));
|
|
this.header.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
|
|
parent.add_child(this.header);
|
|
},
|
|
|
|
_onButtonPressEvent: function (actor, event) {
|
|
this.actor.add_style_pseudo_class('active');
|
|
return Clutter.EVENT_PROPAGATE;
|
|
},
|
|
|
|
_onButtonReleaseEvent: function (actor, event) {
|
|
this.actor.remove_style_pseudo_class('active');
|
|
this.activate(event);
|
|
return Clutter.EVENT_STOP;
|
|
},
|
|
|
|
_onKeyPressEvent: function(actor, event) {
|
|
let symbol = event.get_key_symbol();
|
|
if (symbol == Clutter.KEY_space || symbol == Clutter.KEY_Return) {
|
|
this.activate(event);
|
|
}
|
|
},
|
|
|
|
activate: function(event) {
|
|
if(!this.isOpen) {
|
|
this.open();
|
|
} else {
|
|
this.close();
|
|
}
|
|
},
|
|
|
|
set_child: function(child) {
|
|
if(this.child) {
|
|
this.child.destroy();
|
|
}
|
|
this.scroll.add_actor(child);
|
|
this.child = child;
|
|
let [min, nat] = this.child.get_preferred_width(-1);
|
|
this.scroll.width = nat;
|
|
this.scroll.show();
|
|
this.scroll.height = 0;
|
|
this.child.hide();
|
|
},
|
|
|
|
open: function() {
|
|
if(this.isOpen) {
|
|
return;
|
|
}
|
|
if(!this.child) {
|
|
return;
|
|
}
|
|
this.isOpen = true;
|
|
this.scroll.show();
|
|
this.child.show();
|
|
let targetAngle = 90;
|
|
let [minHeight, naturalHeight] = this.child.get_preferred_height(-1);
|
|
this.scroll.height = 0;
|
|
this.scroll._arrowRotation = this._triangle.rotation_angle_z;
|
|
Tweener.addTween(this.scroll,
|
|
{ _arrowRotation: targetAngle,
|
|
height: naturalHeight,
|
|
time: 0.5,
|
|
onUpdateScope: this,
|
|
onUpdate: function() {
|
|
this._triangle.rotation_angle_z = this.scroll._arrowRotation;
|
|
},
|
|
onCompleteScope: this,
|
|
onComplete: function() {
|
|
this.scroll.set_height(-1);
|
|
}
|
|
});
|
|
},
|
|
|
|
close: function() {
|
|
if(!this.isOpen) {
|
|
return;
|
|
}
|
|
this.isOpen = false;
|
|
this.scroll._arrowRotation = this._triangle.rotation_angle_z;
|
|
Tweener.addTween(this.scroll,
|
|
{ _arrowRotation: 0,
|
|
height: 0,
|
|
time: 0.5,
|
|
onUpdateScope: this,
|
|
onUpdate: function() {
|
|
this._triangle.rotation_angle_z = this.scroll._arrowRotation;
|
|
},
|
|
onCompleteScope: this,
|
|
onComplete: function() {
|
|
this.child.hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
|
|
const PromptDialogHeader = new Lang.Class({
|
|
Name: 'PromptDialogHeader',
|
|
|
|
_init: function() {
|
|
this.actor = new St.BoxLayout();
|
|
let inner = new St.BoxLayout({ vertical: true });
|
|
this.icon = new St.Icon({style_class: 'fw-prompt-icon'})
|
|
this.title = new St.Label({style_class: 'fw-prompt-title'})
|
|
this.message = new St.Label({style_class: 'fw-prompt-message'});
|
|
this.message.clutter_text.line_wrap = true;
|
|
this.message.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
|
inner.add_child(this.title);
|
|
inner.add_child(this.message);
|
|
this.actor.add_child(this.icon);
|
|
this.actor.add_child(inner);
|
|
},
|
|
|
|
setTitle: function(text) {
|
|
if(!text) {
|
|
text = "Unknown";
|
|
}
|
|
this.title.text = text;
|
|
},
|
|
|
|
setMessage: function(text) {
|
|
this.message.text = text;
|
|
},
|
|
|
|
setIcon: function(name) {
|
|
this.icon.icon_name = name;
|
|
},
|
|
|
|
setIconDefault: function() {
|
|
this.setIcon('security-high-symbolic');
|
|
},
|
|
|
|
});
|
|
|
|
const PromptDialog = new Lang.Class({
|
|
Name: 'PromptDialog',
|
|
Extends: ModalDialog.ModalDialog,
|
|
|
|
_init: function(invocation, pid_known) {
|
|
this.parent({ styleClass: 'fw-prompt-dialog' });
|
|
this._invocation = invocation;
|
|
this.header = new PromptDialogHeader();
|
|
this.contentLayout.add_child(this.header.actor);
|
|
|
|
this.details = new ExpandingSection("Details");
|
|
this.contentLayout.add(this.details.actor, {y_fill: false, x_fill: true});
|
|
let box = new St.BoxLayout({ vertical: true });
|
|
this.details.set_child(box);
|
|
this.info = new DetailSection();
|
|
box.add_child(this.info.actor);
|
|
|
|
this.optionList = new OptionList(pid_known);
|
|
box.add_child(this.optionList.actor);
|
|
this.optionList.addOptions([
|
|
"Only PORT AND ADDRESS",
|
|
"Only ADDRESS",
|
|
"Only PORT",
|
|
"Any Connection"]);
|
|
|
|
this._initialKeyFocusDestroyId = 1;
|
|
this.setButtons([
|
|
{ label: "Allow", action: Lang.bind(this, this.onAllow) },
|
|
{ label: "Deny", action: Lang.bind(this, this.onDeny) }
|
|
]);
|
|
},
|
|
|
|
onAllow: function() {
|
|
this.close();
|
|
this.sendReturnValue(true);
|
|
},
|
|
|
|
onDeny: function() {
|
|
this.close();
|
|
this.sendReturnValue(false);
|
|
},
|
|
|
|
sendReturnValue: function(allow) {
|
|
if(!this._invocation) {
|
|
return;
|
|
}
|
|
let verb = "DENY";
|
|
if(allow) {
|
|
verb = "ALLOW";
|
|
}
|
|
let rule = verb + "|" + this.ruleTarget();
|
|
let scope = this.optionList.selectedScope()
|
|
this._invocation.return_value(GLib.Variant.new('(is)', [scope, rule]));
|
|
this._invocation = null;
|
|
},
|
|
|
|
ruleTarget: function() {
|
|
let base = "";
|
|
if(this._proto != "tcp") {
|
|
base = this._proto + ":";
|
|
}
|
|
switch(this.optionList.selectedIdx()) {
|
|
case 0:
|
|
return base + this._address + ":" + this._port;
|
|
case 1:
|
|
return base + this._address + ":*";
|
|
case 2:
|
|
return base + "*:" + this._port;
|
|
case 3:
|
|
return base + "*:*";
|
|
}
|
|
},
|
|
|
|
update: function(application, icon, path, address, port, ip, origin, uid, gid, user, group, pid, proto, optstring, expanded, expert, action) {
|
|
this._address = address;
|
|
this._port = port;
|
|
this._proto = proto;
|
|
|
|
let port_str = (proto+"").toUpperCase() + " Port "+ port;
|
|
|
|
if (proto == "icmp") {
|
|
port_str = (proto+"").toUpperCase() + " Code "+ port;
|
|
}
|
|
|
|
this.header.setTitle(application);
|
|
|
|
if (proto == "tcp") {
|
|
this.header.setMessage("Wants to connect to "+ address + " on " + port_str);
|
|
} else if (proto == "udp") {
|
|
this.header.setMessage("Wants to send data to "+ address + " on " + port_str);
|
|
} else if (proto == "icmp") {
|
|
this.header.setMessage("Wants to send data to "+ address + " with " + port_str);
|
|
}
|
|
|
|
if (expanded) {
|
|
this.details.isOpen = false;
|
|
this.details.activate()
|
|
}
|
|
if(icon) {
|
|
this.header.setIcon(icon);
|
|
} else {
|
|
this.header.setIconDefault();
|
|
}
|
|
|
|
if (proto == "icmp") {
|
|
this.optionList.setOptionText(0, "Only "+ address + " with "+ port_str);
|
|
} else {
|
|
this.optionList.setOptionText(0, "Only "+ address + " on "+ port_str);
|
|
}
|
|
if (expert) {
|
|
|
|
if (proto == "icmp") {
|
|
this.optionList.setOptionText(1, "Only "+ address + " with any ICMP code");
|
|
} else if (proto == "udp") {
|
|
this.optionList.setOptionText(1, "Only "+ address + " on any UDP port");
|
|
} else {
|
|
this.optionList.setOptionText(1, "Only "+ address + " on any port");
|
|
}
|
|
|
|
this.optionList.setOptionText(2, "Only "+ port_str);
|
|
} else {
|
|
this.optionList.setOptionText(1, false);
|
|
this.optionList.setOptionText(2, false);
|
|
}
|
|
|
|
if (proto != "tcp") {
|
|
this.optionList.setOptionText(3, "Any " + proto.toUpperCase() + " data");
|
|
}
|
|
|
|
this.optionList.buttonGroup._setChecked(this.optionList.scopeToIdx(action))
|
|
this.info.setDetails(ip, path, pid, uid, gid, user, group, origin, proto, optstring);
|
|
},
|
|
});
|