Merge remote-tracking branch 'upstream/master' into vive-ui

This commit is contained in:
Brad Davis 2016-06-13 13:03:46 -07:00
commit 0ddee876c3
42 changed files with 1377 additions and 520 deletions

View file

@ -3,9 +3,10 @@ Please read the [general build guide](BUILD.md) for information on dependencies
###Homebrew
[Homebrew](http://brew.sh/) is an excellent package manager for OS X. It makes install of all High Fidelity dependencies very simple.
brew install cmake openssl qt5
brew tap homebrew/versions
brew install cmake openssl qt55
We no longer require install of qt5 via our [homebrew formulas repository](https://github.com/highfidelity/homebrew-formulas). Versions of Qt that are 5.5.x and above provide a mechanism to disable the wireless scanning we previously had a custom patch for.
We no longer require install of qt5 via our [homebrew formulas repository](https://github.com/highfidelity/homebrew-formulas). Versions of Qt that are 5.5.x provide a mechanism to disable the wireless scanning we previously had a custom patch for.
###OpenSSL and Qt

View file

@ -286,8 +286,8 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointer<ReceivedMes
if (!senderID.isNull()) {
// We don't have this node yet - we should add it
matchingNode = DependencyManager::get<LimitedNodeList>()->addOrUpdateNode
(senderID, NodeType::Unassigned, senderSockAddr, senderSockAddr, false, false);
matchingNode = DependencyManager::get<LimitedNodeList>()->addOrUpdateNode(senderID, NodeType::Unassigned,
senderSockAddr, senderSockAddr);
auto childData = std::unique_ptr<AssignmentClientChildData>
{ new AssignmentClientChildData(Assignment::Type::AllTypes) };

View file

@ -235,7 +235,7 @@ void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedN
}
void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanRez()) {
if (senderNode->getCanWriteToAssetServer()) {
QString assetPath = message.readString();
auto assetHash = message.read(SHA256_HASH_LENGTH).toHex();
@ -251,7 +251,7 @@ void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNode
}
void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanRez()) {
if (senderNode->getCanWriteToAssetServer()) {
int numberOfDeletedMappings { 0 };
message.readPrimitive(&numberOfDeletedMappings);
@ -272,7 +272,7 @@ void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, Shared
}
void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanRez()) {
if (senderNode->getCanWriteToAssetServer()) {
QString oldPath = message.readString();
QString newPath = message.readString();
@ -337,7 +337,7 @@ void AssetServer::handleAssetGet(QSharedPointer<ReceivedMessage> message, Shared
void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
if (senderNode->getCanRez()) {
if (senderNode->getCanWriteToAssetServer()) {
qDebug() << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID());
auto task = new UploadAssetTask(message, senderNode, _filesDirectory);

View file

@ -268,6 +268,14 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
qDebug("wantTerseEditLogging=%s", debug::valueOf(wantTerseEditLogging));
EntityTreePointer tree = std::static_pointer_cast<EntityTree>(_tree);
int maxTmpEntityLifetime;
if (readOptionInt("maxTmpLifetime", settingsSectionObject, maxTmpEntityLifetime)) {
tree->setEntityMaxTmpLifetime(maxTmpEntityLifetime);
} else {
tree->setEntityMaxTmpLifetime(EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME);
}
tree->setWantEditLogging(wantEditLogging);
tree->setWantTerseEditLogging(wantTerseEditLogging);
}

View file

@ -1,5 +1,5 @@
{
"version": 1.3,
"version": 1.4,
"settings": [
{
"name": "metaverse",
@ -56,6 +56,7 @@
"label": "Paths",
"help": "Clients can enter a path to reach an exact viewpoint in your domain.<br/>Add rows to the table below to map a path to a viewpoint.<br/>The index path ( / ) is where clients will enter if they do not enter an explicit path.",
"type": "table",
"can_add_new_rows": true,
"key": {
"name": "path",
"label": "Path",
@ -157,27 +158,6 @@
"help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.",
"value-hidden": true
},
{
"name": "restricted_access",
"type": "checkbox",
"label": "Restricted Access",
"default": false,
"help": "Only users listed in \"Allowed Users\" can enter your domain."
},
{
"name": "allowed_users",
"type": "table",
"label": "Allowed Users",
"help": "You can always connect from the domain-server machine.",
"numbered": false,
"columns": [
{
"name": "username",
"label": "Username",
"can_set": true
}
]
},
{
"name": "maximum_user_capacity",
"label": "Maximum User Capacity",
@ -187,25 +167,141 @@
"advanced": false
},
{
"name": "allowed_editors",
"name": "standard_permissions",
"type": "table",
"label": "Allowed Editors",
"help": "List the High Fidelity names for people you want to be able lock or unlock entities in this domain.<br/>An empty list means everyone.",
"numbered": false,
"label": "Domain-Wide User Permissions",
"help": "Indicate which users or groups can have which <a data-toggle='tooltip' data-html=true title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>domain-wide permissions</a>.",
"caption": "Standard Permissions",
"can_add_new_rows": false,
"groups": [
{
"label": "User / Group",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>?</a>",
"span": 6
}
],
"columns": [
{
"name": "username",
"label": "Username",
"can_set": true
"name": "permissions_id",
"label": ""
},
{
"name": "id_can_connect",
"label": "Connect",
"type": "checkbox",
"editable": true,
"default": true
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez",
"label": "Rez",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez_tmp",
"label": "Rez Temporary",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_write_to_asset_server",
"label": "Write Assets",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_connect_past_max_capacity",
"label": "Ignore Max Capacity",
"type": "checkbox",
"editable": true,
"default": false
}
]
],
"non-deletable-row-key": "permissions_id",
"non-deletable-row-values": ["localhost", "anonymous", "logged-in"]
},
{
"name": "editors_are_rezzers",
"type": "checkbox",
"label": "Only Editors Can Create Entities",
"help": "Only users listed in \"Allowed Editors\" can create new entites.",
"default": false
"name": "permissions",
"type": "table",
"caption": "Permissions for Specific Users",
"can_add_new_rows": true,
"groups": [
{
"label": "User / Group",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>?</a>",
"span": 6
}
],
"columns": [
{
"name": "permissions_id",
"label": ""
},
{
"name": "id_can_connect",
"label": "Connect",
"type": "checkbox",
"editable": true,
"default": true
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez",
"label": "Rez",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez_tmp",
"label": "Rez Temporary",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_write_to_asset_server",
"label": "Write Assets",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_connect_past_max_capacity",
"label": "Ignore Max Capacity",
"type": "checkbox",
"editable": true,
"default": false
}
]
}
]
},
@ -218,6 +314,8 @@
"type": "table",
"label": "Persistent Scripts",
"help": "Add the URLs for scripts that you would like to ensure are always running in your domain.",
"can_add_new_rows": true,
"columns": [
{
"name": "url",
@ -302,6 +400,8 @@
"label": "Zones",
"help": "In this table you can define a set of zones in which you can specify various audio properties.",
"numbered": false,
"can_add_new_rows": true,
"key": {
"name": "name",
"label": "Name",
@ -353,6 +453,8 @@
"help": "In this table you can set custom attenuation coefficients between audio zones",
"numbered": true,
"can_order": true,
"can_add_new_rows": true,
"columns": [
{
"name": "source",
@ -380,6 +482,8 @@
"label": "Reverb Settings",
"help": "In this table you can set reverb levels for audio zones. For a medium-sized (e.g., 100 square meter) meeting room, try a decay time of around 1.5 seconds and a wet/dry mix of 25%. For an airplane hangar or cathedral, try a decay time of 4 seconds and a wet/dry mix of 50%.",
"numbered": true,
"can_add_new_rows": true,
"columns": [
{
"name": "zone",
@ -479,6 +583,14 @@
"label": "Entity Server Settings",
"assignment-types": [6],
"settings": [
{
"name": "maxTmpLifetime",
"label": "Maximum Lifetime of Temporary Entities",
"help": "The maximum number of seconds for the lifetime of an entity which will be considered \"temporary\".",
"placeholder": "3600",
"default": "3600",
"advanced": true
},
{
"name": "persistFilePath",
"label": "Entities File Path",
@ -501,6 +613,8 @@
"label": "Backup Rules",
"help": "In this table you can define a set of rules for how frequently to backup copies of your entites content file.",
"numbered": false,
"can_add_new_rows": true,
"default": [
{"Name":"Half Hourly Rolling","backupInterval":1800,"format":".backup.halfhourly.%N","maxBackupVersions":5},
{"Name":"Daily Rolling","backupInterval":86400,"format":".backup.daily.%N","maxBackupVersions":7},

View file

@ -20,6 +20,17 @@ body {
top: 40px;
}
.table .value-row td, .table .inputs td {
vertical-align: middle;
}
.table .table-checkbox {
/* Fix IE sizing checkboxes to fill table cell */
width: auto;
margin-left: auto;
margin-right: auto;
}
.glyphicon-remove {
font-size: 24px;
}
@ -107,6 +118,58 @@ table {
word-wrap: break-word;
}
caption {
color: #333;
font-weight: 700;
padding-top: 0;
}
table > tbody > .headers > td {
vertical-align: middle;
}
table .headers + .headers td {
font-size: 13px;
color: #222;
}
table[name="security.standard_permissions"] .headers td + td, table[name="security.permissions"] .headers td + td {
text-align: center;
}
.tooltip.top .tooltip-arrow {
border-top-color: #fff;
border-width: 10px 10px 0;
margin-bottom: -5px;
}
.tooltip-inner {
padding: 20px 20px 10px 20px;
font-size: 14px;
text-align: left;
color: #333;
background-color: #fff;
box-shadow: 0 3px 8px 8px #e8e8e8;
}
.tooltip.in {
opacity: 1;
}
.tooltip-inner ul {
padding-left: 0;
margin-bottom: 15px;
}
.tooltip-inner li {
list-style-type: none;
margin-bottom: 5px;
}
#security .tooltip-inner {
max-width: 520px;
}
#xs-advanced-container {
margin-bottom: 20px;
}

View file

@ -232,6 +232,17 @@ $(document).ready(function(){
badgeSidebarForDifferences($(this));
});
// Bootstrap switch in table
$('#' + Settings.FORM_ID).on('change', 'input.table-checkbox', function () {
// Bootstrap switches in table: set the changed data attribute for all rows in table.
var row = $(this).closest('tr');
if (row.hasClass("value-row")) { // Don't set attribute on input row switches prior to it being added to table.
row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true);
updateDataChangedForSiblingRows(row, true);
badgeSidebarForDifferences($(this));
}
});
$('.advanced-toggle').click(function(){
Settings.showAdvanced = !Settings.showAdvanced
var advancedSelector = $('.' + Settings.ADVANCED_CLASS)
@ -841,6 +852,8 @@ function reloadSettings(callback) {
// setup any bootstrap switches
$('.toggle-checkbox').bootstrapSwitch();
$('[data-toggle="tooltip"]').tooltip();
// add tooltip to locked settings
$('label.locked').tooltip({
placement: 'right',
@ -875,6 +888,7 @@ function saveSettings() {
}
}
console.log("----- SAVING ------");
console.log(formJSON);
// re-enable all inputs
@ -908,10 +922,33 @@ function makeTable(setting, keypath, setting_value, isLocked) {
html += "<span class='help-block'>" + setting.help + "</span>"
}
var nonDeletableRowKey = setting["non-deletable-row-key"];
var nonDeletableRowValues = setting["non-deletable-row-values"];
html += "<table class='table table-bordered " + (isLocked ? "locked-table" : "") + "' data-short-name='" + setting.name
+ "' name='" + keypath + "' id='" + (typeof setting.html_id !== 'undefined' ? setting.html_id : keypath)
+ "' data-setting-type='" + (isArray ? 'array' : 'hash') + "'>";
if (setting.caption) {
html += "<caption>" + setting.caption + "</caption>"
}
// Column groups
if (setting.groups) {
html += "<tr class='headers'>"
_.each(setting.groups, function (group) {
html += "<td colspan='" + group.span + "'><strong>" + group.label + "</strong></td>"
})
if (!isLocked && !setting.read_only) {
if (setting.can_order) {
html += "<td class='" + Settings.REORDER_BUTTONS_CLASSES +
"'><a href='javascript:void(0);' class='glyphicon glyphicon-sort'></a></td>";
}
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES + "'></td></tr>"
}
html += "</tr>"
}
// Column names
html += "<tr class='headers'>"
@ -950,6 +987,8 @@ function makeTable(setting, keypath, setting_value, isLocked) {
html += "<td class='key'>" + rowIndexOrName + "</td>"
}
var isNonDeletableRow = false;
_.each(setting.columns, function(col) {
if (isArray) {
@ -961,16 +1000,19 @@ function makeTable(setting, keypath, setting_value, isLocked) {
colName = keypath + "." + rowIndexOrName + "." + col.name;
}
// setup the td for this column
html += "<td class='" + Settings.DATA_COL_CLASS + "' name='" + colName + "'>";
isNonDeletableRow = isNonDeletableRow
|| (nonDeletableRowKey === col.name && nonDeletableRowValues.indexOf(colValue) !== -1);
// add the actual value to the td so it is displayed
html += colValue;
if (isArray && col.type === "checkbox" && col.editable) {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>"
+ "<input type='checkbox' class='form-control table-checkbox' "
+ "name='" + colName + "'" + (colValue ? " checked" : "") + " /></td>";
} else {
// Use a hidden input so that the values are posted.
html += "<td class='" + Settings.DATA_COL_CLASS + "' name='" + colName + "'>"
+ colValue + "<input type='hidden' name='" + colName + "' value='" + colValue + "'/></td>";
}
// for values to be posted properly we add a hidden input to this td
html += "<input type='hidden' name='" + colName + "' value='" + colValue + "'/>";
html += "</td>";
})
if (!isLocked && !setting.read_only) {
@ -979,8 +1021,12 @@ function makeTable(setting, keypath, setting_value, isLocked) {
"'><a href='javascript:void(0);' class='" + Settings.MOVE_UP_SPAN_CLASSES + "'></a>"
+ "<a href='javascript:void(0);' class='" + Settings.MOVE_DOWN_SPAN_CLASSES + "'></a></td>"
}
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES +
"'><a href='javascript:void(0);' class='" + Settings.DEL_ROW_SPAN_CLASSES + "'></a></td>"
if (isNonDeletableRow) {
html += "<td></td>";
} else {
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES
+ "'><a href='javascript:void(0);' class='" + Settings.DEL_ROW_SPAN_CLASSES + "'></a></td>";
}
}
html += "</tr>"
@ -990,7 +1036,7 @@ function makeTable(setting, keypath, setting_value, isLocked) {
}
// populate inputs in the table for new values
if (!isLocked && !setting.read_only) {
if (!isLocked && !setting.read_only && setting.can_add_new_rows) {
html += makeTableInputs(setting)
}
html += "</table>"
@ -1012,17 +1058,23 @@ function makeTableInputs(setting) {
}
_.each(setting.columns, function(col) {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>\
<input type='text' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "'\
value='" + (col.default ? col.default : "") + "' data-default='" + (col.default ? col.default : "") + "'>\
</td>"
if (col.type === "checkbox") {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>"
+ "<input type='checkbox' class='form-control table-checkbox' "
+ "name='" + col.name + "'" + (col.default ? " checked" : "") + "/></td>";
} else {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>\
<input type='text' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "'\
value='" + (col.default ? col.default : "") + "' data-default='" + (col.default ? col.default : "") + "'>\
</td>"
}
})
if (setting.can_order) {
html += "<td class='" + Settings.REORDER_BUTTONS_CLASSES + "'></td>"
}
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES +
"'><a href='javascript:void(0);' class='glyphicon glyphicon-plus " + Settings.ADD_ROW_BUTTON_CLASS + "'></a></td>"
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES +
"'><a href='javascript:void(0);' class='glyphicon glyphicon-plus " + Settings.ADD_ROW_BUTTON_CLASS + "'></a></td>"
html += "</tr>"
return html
@ -1127,11 +1179,11 @@ function addTableRow(add_glyphicon) {
} else {
$(element).html(1)
}
} else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) {
$(element).html("<td class='" + Settings.REORDER_BUTTONS_CLASSES + "'><a href='javascript:void(0);'"
+ " class='" + Settings.MOVE_UP_SPAN_CLASSES + "'></a><a href='javascript:void(0);' class='"
+ Settings.MOVE_DOWN_SPAN_CLASSES + "'></span></td>")
} else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) {
} else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) {
$(element).html("<td class='" + Settings.REORDER_BUTTONS_CLASSES + "'><a href='javascript:void(0);'"
+ " class='" + Settings.MOVE_UP_SPAN_CLASSES + "'></a><a href='javascript:void(0);' class='"
+ Settings.MOVE_DOWN_SPAN_CLASSES + "'></span></td>")
} else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) {
// Change buttons
var anchor = $(element).children("a")
anchor.removeClass(Settings.ADD_ROW_SPAN_CLASSES)
@ -1142,8 +1194,20 @@ function addTableRow(add_glyphicon) {
input.remove()
} else if ($(element).hasClass(Settings.DATA_COL_CLASS)) {
// Hide inputs
var input = $(element).children("input")
input.attr("type", "hidden")
var input = $(element).find("input")
var isCheckbox = false;
if (input.hasClass("table-checkbox")) {
input = $(input).parent();
isCheckbox = true;
}
var val = input.val();
if (isCheckbox) {
val = $(input).find("input").is(':checked');
// don't hide the checkbox
} else {
input.attr("type", "hidden")
}
if (isArray) {
var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length
@ -1152,14 +1216,22 @@ function addTableRow(add_glyphicon) {
// are there multiple columns or just one?
// with multiple we have an array of Objects, with one we have an array of whatever the value type is
var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
if (isCheckbox) {
$(input).find("input").attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
} else {
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
}
} else {
input.attr("name", full_name + "." + $(element).attr("name"))
}
input.attr("data-changed", "true")
$(element).append(input.val())
if (isCheckbox) {
$(input).find("input").attr("data-changed", "true");
} else {
input.attr("data-changed", "true");
$(element).append(val);
}
} else {
console.log("Unknown table element")
}

View file

@ -26,7 +26,7 @@ using SharedAssignmentPointer = QSharedPointer<Assignment>;
DomainGatekeeper::DomainGatekeeper(DomainServer* server) :
_server(server)
{
}
void DomainGatekeeper::addPendingAssignedNode(const QUuid& nodeUUID, const QUuid& assignmentUUID,
@ -38,7 +38,7 @@ void DomainGatekeeper::addPendingAssignedNode(const QUuid& nodeUUID, const QUuid
QUuid DomainGatekeeper::assignmentUUIDForPendingAssignment(const QUuid& tempUUID) {
auto it = _pendingAssignedNodes.find(tempUUID);
if (it != _pendingAssignedNodes.end()) {
return it->second.getAssignmentUUID();
} else {
@ -62,59 +62,56 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
QByteArray myProtocolVersion = protocolVersionsSignature();
if (nodeConnection.protocolVersion != myProtocolVersion) {
QString protocolVersionError = "Protocol version mismatch - Domain version:" + QCoreApplication::applicationVersion();
qDebug() << "Protocol Version mismatch - denying connection.";
sendConnectionDeniedPacket(protocolVersionError, message->getSenderSockAddr(),
DomainHandler::ConnectionRefusedReason::ProtocolMismatch);
sendProtocolMismatchConnectionDenial(message->getSenderSockAddr());
return;
}
if (nodeConnection.localSockAddr.isNull() || nodeConnection.publicSockAddr.isNull()) {
qDebug() << "Unexpected data received for node local socket or public socket. Will not allow connection.";
return;
}
static const NodeSet VALID_NODE_TYPES {
NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::AssetServer, NodeType::EntityServer, NodeType::Agent, NodeType::MessagesMixer
};
if (!VALID_NODE_TYPES.contains(nodeConnection.nodeType)) {
qDebug() << "Received an invalid node type with connect request. Will not allow connection from"
<< nodeConnection.senderSockAddr << ": " << nodeConnection.nodeType;
return;
}
// check if this connect request matches an assignment in the queue
auto pendingAssignment = _pendingAssignedNodes.find(nodeConnection.connectUUID);
SharedNodePointer node;
if (pendingAssignment != _pendingAssignedNodes.end()) {
node = processAssignmentConnectRequest(nodeConnection, pendingAssignment->second);
} else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) {
QString username;
QByteArray usernameSignature;
if (message->getBytesLeftToRead() > 0) {
// read username from packet
packetStream >> username;
if (message->getBytesLeftToRead() > 0) {
// read user signature from packet
packetStream >> usernameSignature;
}
}
node = processAgentConnectRequest(nodeConnection, username, usernameSignature);
}
if (node) {
// set the sending sock addr and node interest set on this node
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
nodeData->setSendingSockAddr(message->getSenderSockAddr());
nodeData->setNodeInterestSet(nodeConnection.interestList.toSet());
nodeData->setPlaceName(nodeConnection.placeName);
// signal that we just connected a node so the DomainServer can get it a list
// and broadcast its presence right away
emit connectedNode(node);
@ -123,18 +120,72 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
}
}
void DomainGatekeeper::updateNodePermissions() {
// If the permissions were changed on the domain-server webpage (and nothing else was), a restart isn't required --
// we reprocess the permissions map and update the nodes here. The node list is frequently sent out to all
// the connected nodes, so these changes are propagated to other nodes.
QList<SharedNodePointer> nodesToKill;
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
limitedNodeList->eachNode([this, limitedNodeList, &nodesToKill](const SharedNodePointer& node){
QString username = node->getPermissions().getUserName();
NodePermissions userPerms(username);
if (node->getPermissions().isAssignment) {
// this node is an assignment-client
userPerms.isAssignment = true;
userPerms.canAdjustLocks = true;
userPerms.canRezPermanentEntities = true;
userPerms.canRezTemporaryEntities = true;
} else {
// this node is an agent
userPerms.setAll(false);
const QHostAddress& addr = node->getLocalSocket().getAddress();
bool isLocalUser = (addr == limitedNodeList->getLocalSockAddr().getAddress() ||
addr == QHostAddress::LocalHost);
if (isLocalUser) {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost);
}
if (username.isEmpty()) {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous);
} else {
if (_server->_settingsManager.havePermissionsForName(username)) {
userPerms = _server->_settingsManager.getPermissionsForName(username);
} else {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn);
}
}
}
node->setPermissions(userPerms);
if (!userPerms.canConnectToDomain) {
qDebug() << "node" << node->getUUID() << "no longer has permission to connect.";
// hang up on this node
nodesToKill << node;
}
});
foreach (auto node, nodesToKill) {
emit killNode(node);
}
}
SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeConnectionData& nodeConnection,
const PendingAssignedNodeData& pendingAssignment) {
// make sure this matches an assignment the DS told us we sent out
auto it = _pendingAssignedNodes.find(nodeConnection.connectUUID);
SharedAssignmentPointer matchingQueuedAssignment = SharedAssignmentPointer();
if (it != _pendingAssignedNodes.end()) {
// find the matching queued static assignment in DS queue
matchingQueuedAssignment = _server->dequeueMatchingAssignment(it->second.getAssignmentUUID(), nodeConnection.nodeType);
if (matchingQueuedAssignment) {
qDebug() << "Assignment deployed with" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID)
<< "matches unfulfilled assignment"
@ -149,124 +200,99 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo
qDebug() << "No assignment was deployed with UUID" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID);
return SharedNodePointer();
}
// add the new node
SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection);
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(newNode->getLinkedData());
// set assignment related data on the linked data for this node
nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID());
nodeData->setWalletUUID(it->second.getWalletUUID());
nodeData->setNodeVersion(it->second.getNodeVersion());
nodeData->setWasAssigned(true);
// cleanup the PendingAssignedNodeData for this assignment now that it's connecting
_pendingAssignedNodes.erase(it);
// always allow assignment clients to create and destroy entities
newNode->setIsAllowedEditor(true);
newNode->setCanRez(true);
NodePermissions userPerms;
userPerms.isAssignment = true;
userPerms.canAdjustLocks = true;
userPerms.canRezPermanentEntities = true;
userPerms.canRezTemporaryEntities = true;
newNode->setPermissions(userPerms);
return newNode;
}
const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity";
const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors";
const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers";
SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnectionData& nodeConnection,
const QString& username,
const QByteArray& usernameSignature) {
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
bool isRestrictingAccess =
_server->_settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool();
// check if this user is on our local machine - if this is true they are always allowed to connect
// start with empty permissions
NodePermissions userPerms(username);
userPerms.setAll(false);
// check if this user is on our local machine - if this is true set permissions to those for a "localhost" connection
QHostAddress senderHostAddress = nodeConnection.senderSockAddr.getAddress();
bool isLocalUser =
(senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost);
// if we're using restricted access and this user is not local make sure we got a user signature
if (isRestrictingAccess && !isLocalUser) {
if (!username.isEmpty()) {
if (usernameSignature.isEmpty()) {
// if user didn't include usernameSignature in connect request, send a connectionToken packet
sendConnectionTokenPacket(username, nodeConnection.senderSockAddr);
// ask for their public key right now to make sure we have it
requestUserPublicKey(username);
return SharedNodePointer();
}
}
if (isLocalUser) {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost);
qDebug() << "user-permissions: is local user, so:" << userPerms;
}
bool verifiedUsername = false;
// if we do not have a local user we need to subject them to our verification and capacity checks
if (!isLocalUser) {
// check if we need to look at the username signature
if (isRestrictingAccess) {
if (isVerifiedAllowedUser(username, usernameSignature, nodeConnection.senderSockAddr)) {
// we verified the user via their username and signature - set the verifiedUsername
// so we don't re-decrypt their sig if we're trying to exempt them from max capacity check (due to
// being in the allowed editors list)
verifiedUsername = true;
} else {
// failed to verify user - return a null shared ptr
return SharedNodePointer();
}
}
if (!isWithinMaxCapacity(username, usernameSignature, verifiedUsername, nodeConnection.senderSockAddr)) {
// we can't allow this user to connect because we are at max capacity (and they either aren't an allowed editor
// or couldn't be verified as one)
if (!username.isEmpty() && usernameSignature.isEmpty()) {
// user is attempting to prove their identity to us, but we don't have enough information
sendConnectionTokenPacket(username, nodeConnection.senderSockAddr);
// ask for their public key right now to make sure we have it
requestUserPublicKey(username);
if (!userPerms.canConnectToDomain) {
return SharedNodePointer();
}
}
// if this user is in the editors list (or if the editors list is empty) set the user's node's isAllowedEditor to true
const QVariant* allowedEditorsVariant =
valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH);
QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList();
// if the allowed editors list is empty then everyone can adjust locks
bool isAllowedEditor = allowedEditors.empty();
if (allowedEditors.contains(username, Qt::CaseInsensitive)) {
// we have a non-empty allowed editors list - check if this user is verified to be in it
if (!verifiedUsername) {
if (!verifyUserSignature(username, usernameSignature, HifiSockAddr())) {
// failed to verify a user that is in the allowed editors list
// TODO: fix public key refresh in interface/metaverse and force this check
qDebug() << "Could not verify user" << username << "as allowed editor. In the interim this user"
<< "will be given edit rights to avoid a thrasing of public key requests and connect requests.";
}
isAllowedEditor = true;
if (username.isEmpty()) {
// they didn't tell us who they are
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous);
qDebug() << "user-permissions: no username, so:" << userPerms;
} else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) {
// they are sent us a username and the signature verifies it
if (_server->_settingsManager.havePermissionsForName(username)) {
// we have specific permissions for this user.
userPerms = _server->_settingsManager.getPermissionsForName(username);
qDebug() << "user-permissions: specific user matches, so:" << userPerms;
} else {
// already verified this user and they are in the allowed editors list
isAllowedEditor = true;
// they are logged into metaverse, but we don't have specific permissions for them.
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn);
qDebug() << "user-permissions: user is logged in, so:" << userPerms;
}
userPerms.setUserName(username);
} else {
// they sent us a username, but it didn't check out
requestUserPublicKey(username);
if (!userPerms.canConnectToDomain) {
return SharedNodePointer();
}
}
// check if only editors should be able to rez entities
const QVariant* editorsAreRezzersVariant =
valueForKeyPath(_server->_settingsManager.getSettingsMap(), EDITORS_ARE_REZZERS_KEYPATH);
bool onlyEditorsAreRezzers = false;
if (editorsAreRezzersVariant) {
onlyEditorsAreRezzers = editorsAreRezzersVariant->toBool();
qDebug() << "user-permissions: final:" << userPerms;
if (!userPerms.canConnectToDomain) {
sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.",
nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::TooManyUsers);
return SharedNodePointer();
}
bool canRez = true;
if (onlyEditorsAreRezzers) {
canRez = isAllowedEditor;
if (!userPerms.canConnectPastMaxCapacity && !isWithinMaxCapacity()) {
// we can't allow this user to connect because we are at max capacity
sendConnectionDeniedPacket("Too many connected users.", nodeConnection.senderSockAddr,
DomainHandler::ConnectionRefusedReason::TooManyUsers);
return SharedNodePointer();
}
QUuid hintNodeID;
@ -285,24 +311,23 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
return true;
});
// add the connecting node (or re-use the matched one from eachNodeBreakable above)
SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection, hintNodeID);
// set the edit rights for this user
newNode->setIsAllowedEditor(isAllowedEditor);
newNode->setCanRez(canRez);
newNode->setPermissions(userPerms);
// grab the linked data for our new node so we can set the username
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(newNode->getLinkedData());
// if we have a username from the connect request, set it on the DomainServerNodeData
nodeData->setUsername(username);
// also add an interpolation to DomainServerNodeData so that servers can get username in stats
nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY,
uuidStringWithoutCurlyBraces(newNode->getUUID()), username);
return newNode;
}
@ -310,11 +335,11 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node
QUuid nodeID) {
HifiSockAddr discoveredSocket = nodeConnection.senderSockAddr;
SharedNetworkPeer connectedPeer = _icePeers.value(nodeConnection.connectUUID);
if (connectedPeer) {
// this user negotiated a connection with us via ICE, so re-use their ICE client ID
nodeID = nodeConnection.connectUUID;
if (connectedPeer->getActiveSocket()) {
// set their discovered socket to whatever the activated socket on the network peer object was
discoveredSocket = *connectedPeer->getActiveSocket();
@ -325,15 +350,15 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node
nodeID = QUuid::createUuid();
}
}
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
SharedNodePointer newNode = limitedNodeList->addOrUpdateNode(nodeID, nodeConnection.nodeType,
nodeConnection.publicSockAddr, nodeConnection.localSockAddr);
// So that we can send messages to this node at will - we need to activate the correct socket on this node now
newNode->activateMatchingOrNewSymmetricSocket(discoveredSocket);
return newNode;
}
@ -343,21 +368,21 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
// it's possible this user can be allowed to connect, but we need to check their username signature
QByteArray publicKeyArray = _userPublicKeys.value(username);
const QUuid& connectionToken = _connectionTokenHash.value(username.toLower());
if (!publicKeyArray.isEmpty() && !connectionToken.isNull()) {
// if we do have a public key for the user, check for a signature match
const unsigned char* publicKeyData = reinterpret_cast<const unsigned char*>(publicKeyArray.constData());
// first load up the public key into an RSA struct
RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, publicKeyArray.size());
QByteArray lowercaseUsername = username.toLower().toUtf8();
QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsername.append(connectionToken.toRfc4122()),
QCryptographicHash::Sha256);
if (rsaPublicKey) {
int decryptResult = RSA_verify(NID_sha256,
reinterpret_cast<const unsigned char*>(usernameWithToken.constData()),
@ -365,29 +390,29 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
reinterpret_cast<const unsigned char*>(usernameSignature.constData()),
usernameSignature.size(),
rsaPublicKey);
if (decryptResult == 1) {
qDebug() << "Username signature matches for" << username << "- allowing connection.";
qDebug() << "Username signature matches for" << username;
// free up the public key and remove connection token before we return
RSA_free(rsaPublicKey);
_connectionTokenHash.remove(username);
return true;
} else {
if (!senderSockAddr.isNull()) {
qDebug() << "Error decrypting username signature for " << username << "- denying connection.";
sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr,
DomainHandler::ConnectionRefusedReason::LoginError);
}
// free up the public key, we don't need it anymore
RSA_free(rsaPublicKey);
}
} else {
// we can't let this user in since we couldn't convert their public key to an RSA key we could use
if (!senderSockAddr.isNull()) {
qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection.";
@ -402,86 +427,35 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
DomainHandler::ConnectionRefusedReason::LoginError);
}
}
requestUserPublicKey(username); // no joy. maybe next time?
return false;
}
bool DomainGatekeeper::isVerifiedAllowedUser(const QString& username, const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr) {
if (username.isEmpty()) {
qDebug() << "Connect request denied - no username provided.";
sendConnectionDeniedPacket("No username provided", senderSockAddr,
DomainHandler::ConnectionRefusedReason::LoginError);
return false;
}
QStringList allowedUsers =
_server->_settingsManager.valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList();
if (allowedUsers.contains(username, Qt::CaseInsensitive)) {
if (!verifyUserSignature(username, usernameSignature, senderSockAddr)) {
return false;
}
} else {
qDebug() << "Connect request denied for user" << username << "- not in allowed users list.";
sendConnectionDeniedPacket("User not on whitelist.", senderSockAddr,
DomainHandler::ConnectionRefusedReason::NotAuthorized);
return false;
}
return true;
}
bool DomainGatekeeper::isWithinMaxCapacity(const QString& username, const QByteArray& usernameSignature,
bool& verifiedUsername,
const HifiSockAddr& senderSockAddr) {
bool DomainGatekeeper::isWithinMaxCapacity() {
// find out what our maximum capacity is
const QVariant* maximumUserCapacityVariant = valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY);
const QVariant* maximumUserCapacityVariant =
valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY);
unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0;
if (maximumUserCapacity > 0) {
unsigned int connectedUsers = _server->countConnectedUsers();
if (connectedUsers >= maximumUserCapacity) {
// too many users, deny the new connection unless this user is an allowed editor
const QVariant* allowedEditorsVariant =
valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH);
QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList();
if (allowedEditors.contains(username)) {
if (verifiedUsername || verifyUserSignature(username, usernameSignature, senderSockAddr)) {
verifiedUsername = true;
qDebug() << "Above maximum capacity -" << connectedUsers << "/" << maximumUserCapacity <<
"but user" << username << "is in allowed editors list so will be allowed to connect.";
return true;
}
}
// deny connection from this user
qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, denying new connection.";
sendConnectionDeniedPacket("Too many connected users.", senderSockAddr,
DomainHandler::ConnectionRefusedReason::TooManyUsers);
return false;
}
qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, allowing new connection.";
}
return true;
}
void DomainGatekeeper::preloadAllowedUserPublicKeys() {
const QVariant* allowedUsersVariant = valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_USERS_SETTINGS_KEYPATH);
QStringList allowedUsers = allowedUsersVariant ? allowedUsersVariant->toStringList() : QStringList();
QStringList allowedUsers = _server->_settingsManager.getAllNames();
if (allowedUsers.size() > 0) {
// in the future we may need to limit how many requests here - for now assume that lists of allowed users are not
// going to create > 100 requests
@ -492,15 +466,20 @@ void DomainGatekeeper::preloadAllowedUserPublicKeys() {
}
void DomainGatekeeper::requestUserPublicKey(const QString& username) {
// don't request public keys for the standard psuedo-account-names
if (NodePermissions::standardNames.contains(username, Qt::CaseInsensitive)) {
return;
}
// even if we have a public key for them right now, request a new one in case it has just changed
JSONCallbackParameters callbackParams;
callbackParams.jsonCallbackReceiver = this;
callbackParams.jsonCallbackMethod = "publicKeyJSONCallback";
const QString USER_PUBLIC_KEY_PATH = "api/v1/users/%1/public_key";
qDebug() << "Requesting public key for user" << username;
DependencyManager::get<AccountManager>()->sendRequest(USER_PUBLIC_KEY_PATH.arg(username),
AccountManagerAuth::None,
QNetworkAccessManager::GetOperation, callbackParams);
@ -508,38 +487,47 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) {
void DomainGatekeeper::publicKeyJSONCallback(QNetworkReply& requestReply) {
QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
if (jsonObject["status"].toString() == "success") {
// figure out which user this is for
const QString PUBLIC_KEY_URL_REGEX_STRING = "api\\/v1\\/users\\/([A-Za-z0-9_\\.]+)\\/public_key";
QRegExp usernameRegex(PUBLIC_KEY_URL_REGEX_STRING);
if (usernameRegex.indexIn(requestReply.url().toString()) != -1) {
QString username = usernameRegex.cap(1);
qDebug() << "Storing a public key for user" << username;
// pull the public key as a QByteArray from this response
const QString JSON_DATA_KEY = "data";
const QString JSON_PUBLIC_KEY_KEY = "public_key";
_userPublicKeys[username] =
QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8());
}
}
}
void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr,
DomainHandler::ConnectionRefusedReason reasonCode) {
void DomainGatekeeper::sendProtocolMismatchConnectionDenial(const HifiSockAddr& senderSockAddr) {
QString protocolVersionError = "Protocol version mismatch - Domain version: " + QCoreApplication::applicationVersion();
qDebug() << "Protocol Version mismatch - denying connection.";
sendConnectionDeniedPacket(protocolVersionError, senderSockAddr,
DomainHandler::ConnectionRefusedReason::ProtocolMismatch);
}
void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr,
DomainHandler::ConnectionRefusedReason reasonCode) {
// this is an agent and we've decided we won't let them connect - send them a packet to deny connection
QByteArray utfString = reason.toUtf8();
quint16 payloadSize = utfString.size();
// setup the DomainConnectionDenied packet
auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied,
payloadSize + sizeof(payloadSize) + sizeof(uint8_t));
auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied,
payloadSize + sizeof(payloadSize) + sizeof(uint8_t));
// pack in the reason the connection was denied (the client displays this)
if (payloadSize > 0) {
uint8_t reasonCodeWire = (uint8_t)reasonCode;
@ -547,7 +535,7 @@ void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const H
connectionDeniedPacket->writePrimitive(payloadSize);
connectionDeniedPacket->write(utfString);
}
// send the packet off
DependencyManager::get<LimitedNodeList>()->sendPacket(std::move(connectionDeniedPacket), senderSockAddr);
}
@ -555,20 +543,20 @@ void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const H
void DomainGatekeeper::sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr) {
// get the existing connection token or create a new one
QUuid& connectionToken = _connectionTokenHash[username.toLower()];
if (connectionToken.isNull()) {
connectionToken = QUuid::createUuid();
}
// setup a static connection token packet
static auto connectionTokenPacket = NLPacket::create(PacketType::DomainServerConnectionToken, NUM_BYTES_RFC4122_UUID);
// reset the packet before each time we send
connectionTokenPacket->reset();
// write the connection token
connectionTokenPacket->write(connectionToken.toRfc4122());
// send off the packet unreliably
DependencyManager::get<LimitedNodeList>()->sendUnreliablePacket(*connectionTokenPacket, senderSockAddr);
}
@ -576,33 +564,33 @@ void DomainGatekeeper::sendConnectionTokenPacket(const QString& username, const
const int NUM_PEER_PINGS_BEFORE_DELETE = 2000 / UDP_PUNCH_PING_INTERVAL_MS;
void DomainGatekeeper::pingPunchForConnectingPeer(const SharedNetworkPeer& peer) {
if (peer->getConnectionAttempts() >= NUM_PEER_PINGS_BEFORE_DELETE) {
// we've reached the maximum number of ping attempts
qDebug() << "Maximum number of ping attempts reached for peer with ID" << peer->getUUID();
qDebug() << "Removing from list of connecting peers.";
_icePeers.remove(peer->getUUID());
} else {
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
// send the ping packet to the local and public sockets for this node
auto localPingPacket = limitedNodeList->constructICEPingPacket(PingType::Local, limitedNodeList->getSessionUUID());
limitedNodeList->sendPacket(std::move(localPingPacket), peer->getLocalSocket());
auto publicPingPacket = limitedNodeList->constructICEPingPacket(PingType::Public, limitedNodeList->getSessionUUID());
limitedNodeList->sendPacket(std::move(publicPingPacket), peer->getPublicSocket());
peer->incrementConnectionAttempts();
}
}
void DomainGatekeeper::handlePeerPingTimeout() {
NetworkPeer* senderPeer = qobject_cast<NetworkPeer*>(sender());
if (senderPeer) {
SharedNetworkPeer sharedPeer = _icePeers.value(senderPeer->getUUID());
if (sharedPeer && !sharedPeer->getActiveSocket()) {
pingPunchForConnectingPeer(sharedPeer);
}
@ -613,24 +601,24 @@ void DomainGatekeeper::processICEPeerInformationPacket(QSharedPointer<ReceivedMe
// loop through the packet and pull out network peers
// any peer we don't have we add to the hash, otherwise we update
QDataStream iceResponseStream(message->getMessage());
NetworkPeer* receivedPeer = new NetworkPeer;
iceResponseStream >> *receivedPeer;
if (!_icePeers.contains(receivedPeer->getUUID())) {
qDebug() << "New peer requesting ICE connection being added to hash -" << *receivedPeer;
SharedNetworkPeer newPeer = SharedNetworkPeer(receivedPeer);
_icePeers[receivedPeer->getUUID()] = newPeer;
// make sure we know when we should ping this peer
connect(newPeer.data(), &NetworkPeer::pingTimerTimeout, this, &DomainGatekeeper::handlePeerPingTimeout);
// immediately ping the new peer, and start a timer to continue pinging it until we connect to it
newPeer->startPingTimer();
qDebug() << "Sending ping packets to establish connectivity with ICE peer with ID"
<< newPeer->getUUID();
pingPunchForConnectingPeer(newPeer);
} else {
delete receivedPeer;
@ -640,18 +628,18 @@ void DomainGatekeeper::processICEPeerInformationPacket(QSharedPointer<ReceivedMe
void DomainGatekeeper::processICEPingPacket(QSharedPointer<ReceivedMessage> message) {
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
auto pingReplyPacket = limitedNodeList->constructICEPingReplyPacket(*message, limitedNodeList->getSessionUUID());
limitedNodeList->sendPacket(std::move(pingReplyPacket), message->getSenderSockAddr());
}
void DomainGatekeeper::processICEPingReplyPacket(QSharedPointer<ReceivedMessage> message) {
QDataStream packetStream(message->getMessage());
QUuid nodeUUID;
packetStream >> nodeUUID;
SharedNetworkPeer sendingPeer = _icePeers.value(nodeUUID);
if (sendingPeer) {
// we had this NetworkPeer in our connecting list - add the right sock addr to our connected list
sendingPeer->activateMatchingOrNewSymmetricSocket(message->getSenderSockAddr());

View file

@ -42,6 +42,8 @@ public:
void preloadAllowedUserPublicKeys();
void removeICEPeer(const QUuid& peerUUID) { _icePeers.remove(peerUUID); }
static void sendProtocolMismatchConnectionDenial(const HifiSockAddr& senderSockAddr);
public slots:
void processConnectRequestPacket(QSharedPointer<ReceivedMessage> message);
void processICEPingPacket(QSharedPointer<ReceivedMessage> message);
@ -51,8 +53,12 @@ public slots:
void publicKeyJSONCallback(QNetworkReply& requestReply);
signals:
void killNode(SharedNodePointer node);
void connectedNode(SharedNodePointer node);
public slots:
void updateNodePermissions();
private slots:
void handlePeerPingTimeout();
private:
@ -66,18 +72,14 @@ private:
bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr);
bool isVerifiedAllowedUser(const QString& username, const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr);
bool isWithinMaxCapacity(const QString& username, const QByteArray& usernameSignature,
bool& verifiedUsername,
const HifiSockAddr& senderSockAddr);
bool isWithinMaxCapacity();
bool shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr);
void sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr);
void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr,
DomainHandler::ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown);
static void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr,
DomainHandler::ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown);
void pingPunchForConnectingPeer(const SharedNetworkPeer& peer);

View file

@ -101,6 +101,13 @@ DomainServer::DomainServer(int argc, char* argv[]) :
// make sure we hear about newly connected nodes from our gatekeeper
connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode);
// if a connected node loses connection privileges, hang up on it
connect(&_gatekeeper, &DomainGatekeeper::killNode, this, &DomainServer::handleKillNode);
// if permissions are updated, relay the changes to the Node datastructures
connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions,
&_gatekeeper, &DomainGatekeeper::updateNodePermissions);
if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) {
// we either read a certificate and private key or were not passed one
// and completed login or did not need to
@ -318,16 +325,11 @@ bool DomainServer::packetVersionMatch(const udt::Packet& packet) {
auto nodeList = DependencyManager::get<LimitedNodeList>();
// This implements a special case that handles OLD clients which don't know how to negotiate matching
// protocol versions. We know these clients will sent DomainConnectRequest with older versions. We also
// know these clients will show a warning dialog if they get an EntityData with a protocol version they
// don't understand, so we can send them an empty EntityData with our latest version and they will
// warn the user that the protocol is not compatible
if (headerType == PacketType::DomainConnectRequest &&
headerVersion < static_cast<PacketVersion>(DomainConnectRequestVersion::HasProtocolVersions)) {
auto packetWithBadVersion = NLPacket::create(PacketType::EntityData);
nodeList->sendPacket(std::move(packetWithBadVersion), packet.getSenderSockAddr());
return false;
// if this is a mismatching connect packet, we can't simply drop it on the floor
// send back a packet to the interface that tells them we refuse connection for a mismatch
if (headerType == PacketType::DomainConnectRequest
&& headerVersion != versionForPacketType(PacketType::DomainConnectRequest)) {
DomainGatekeeper::sendProtocolMismatchConnectionDenial(packet.getSenderSockAddr());
}
// let the normal nodeList implementation handle all other packets.
@ -800,8 +802,7 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
extendedHeaderStream << limitedNodeList->getSessionUUID();
extendedHeaderStream << node->getUUID();
extendedHeaderStream << (quint8) node->isAllowedEditor();
extendedHeaderStream << (quint8) node->getCanRez();
extendedHeaderStream << node->getPermissions();
auto domainListPackets = NLPacketList::create(PacketType::DomainList, extendedHeader);
@ -1093,11 +1094,12 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) {
static const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking";
domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting;
// Add a flag to indicate if this domain uses restricted access -
// for now that will exclude it from listings
static const QString RESTRICTED_ACCESS_FLAG = "restricted";
domainObject[RESTRICTED_ACCESS_FLAG] =
_settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool();
// add a flag to indicate if this domain uses restricted access - for now that will exclude it from listings
const QString RESTRICTED_ACCESS_FLAG = "restricted";
// consider the domain to have restricted access if "anonymous" connections can't connect to the domain.
NodePermissions anonymousPermissions = _settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous);
domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.canConnectToDomain;
// Add the metadata to the heartbeat
static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat";
@ -2107,35 +2109,42 @@ void DomainServer::processPathQueryPacket(QSharedPointer<ReceivedMessage> messag
void DomainServer::processNodeDisconnectRequestPacket(QSharedPointer<ReceivedMessage> message) {
// This packet has been matched to a source node and they're asking not to be in the domain anymore
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
const QUuid& nodeUUID = message->getSourceID();
qDebug() << "Received a disconnect request from node with UUID" << nodeUUID;
// we want to check what type this node was before going to kill it so that we can avoid sending the RemovedNode
// packet to nodes that don't care about this type
auto nodeToKill = limitedNodeList->nodeWithUUID(nodeUUID);
if (nodeToKill) {
auto nodeType = nodeToKill->getType();
limitedNodeList->killNodeWithUUID(nodeUUID);
static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID);
removedNodePacket->reset();
removedNodePacket->write(nodeUUID.toRfc4122());
// broadcast out the DomainServerRemovedNode message
limitedNodeList->eachMatchingNode([&nodeType](const SharedNodePointer& otherNode) -> bool {
// only send the removed node packet to nodes that care about the type of node this was
auto nodeLinkedData = dynamic_cast<DomainServerNodeData*>(otherNode->getLinkedData());
return (nodeLinkedData != nullptr) && nodeLinkedData->getNodeInterestSet().contains(nodeType);
}, [&limitedNodeList](const SharedNodePointer& otherNode){
limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode);
});
handleKillNode(nodeToKill);
}
}
void DomainServer::handleKillNode(SharedNodePointer nodeToKill) {
auto nodeType = nodeToKill->getType();
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
const QUuid& nodeUUID = nodeToKill->getUUID();
limitedNodeList->killNodeWithUUID(nodeUUID);
static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID);
removedNodePacket->reset();
removedNodePacket->write(nodeUUID.toRfc4122());
// broadcast out the DomainServerRemovedNode message
limitedNodeList->eachMatchingNode([&nodeType](const SharedNodePointer& otherNode) -> bool {
// only send the removed node packet to nodes that care about the type of node this was
auto nodeLinkedData = dynamic_cast<DomainServerNodeData*>(otherNode->getLinkedData());
return (nodeLinkedData != nullptr) && nodeLinkedData->getNodeInterestSet().contains(nodeType);
}, [&limitedNodeList](const SharedNodePointer& otherNode){
limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode);
});
}
void DomainServer::processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> message) {
static const int NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN = 3;

View file

@ -114,6 +114,8 @@ private:
unsigned int countConnectedUsers();
void handleKillNode(SharedNodePointer nodeToKill);
void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr);
QUuid connectionSecretForNodes(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB);

View file

@ -9,6 +9,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <algorithm>
#include <QtCore/QCoreApplication>
#include <QtCore/QDir>
#include <QtCore/QFile>
@ -26,6 +28,9 @@
#include "DomainServerSettingsManager.h"
#define WANT_DEBUG 1
const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json";
const QString DESCRIPTION_SETTINGS_KEY = "settings";
@ -44,7 +49,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() :
QFile descriptionFile(QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH);
descriptionFile.open(QIODevice::ReadOnly);
QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll());
QJsonParseError parseError;
QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll(), &parseError);
if (descriptionDocument.isObject()) {
QJsonObject descriptionObject = descriptionDocument.object();
@ -63,8 +69,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() :
}
static const QString MISSING_SETTINGS_DESC_MSG =
QString("Did not find settings decription in JSON at %1 - Unable to continue. domain-server will quit.")
.arg(SETTINGS_DESCRIPTION_RELATIVE_PATH);
QString("Did not find settings decription in JSON at %1 - Unable to continue. domain-server will quit.\n%2 at %3")
.arg(SETTINGS_DESCRIPTION_RELATIVE_PATH).arg(parseError.errorString()).arg(parseError.offset);
static const int MISSING_SETTINGS_DESC_ERROR_CODE = 6;
QMetaObject::invokeMethod(QCoreApplication::instance(), "queuedQuit", Qt::QueuedConnection,
@ -88,7 +94,8 @@ void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointer<Re
}
void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList) {
_configMap.loadMasterAndUserConfig(argumentList);
_argumentList = argumentList;
_configMap.loadMasterAndUserConfig(_argumentList);
// What settings version were we before and what are we using now?
// Do we need to do any re-mapping?
@ -97,6 +104,11 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
double oldVersion = appSettings.value(JSON_SETTINGS_VERSION_KEY, 0.0).toDouble();
if (oldVersion != _descriptionVersion) {
const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users";
const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access";
const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors";
const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers";
qDebug() << "Previous domain-server settings version was"
<< QString::number(oldVersion, 'g', 8) << "and the new version is"
<< QString::number(_descriptionVersion, 'g', 8) << "- checking if any re-mapping is required";
@ -127,7 +139,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
persistToFile();
// reload the master and user config so that the merged config is right
_configMap.loadMasterAndUserConfig(argumentList);
_configMap.loadMasterAndUserConfig(_argumentList);
}
}
@ -163,7 +175,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
persistToFile();
// reload the master and user config so that the merged config is right
_configMap.loadMasterAndUserConfig(argumentList);
_configMap.loadMasterAndUserConfig(_argumentList);
}
}
@ -186,15 +198,216 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
persistToFile();
// reload the master and user config so the merged config is correct
_configMap.loadMasterAndUserConfig(argumentList);
_configMap.loadMasterAndUserConfig(_argumentList);
}
}
if (oldVersion < 1.4) {
// This was prior to the permissions-grid in the domain-server settings page
bool isRestrictedAccess = valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool();
QStringList allowedUsers = valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList();
QStringList allowedEditors = valueOrDefaultValueForKeyPath(ALLOWED_EDITORS_SETTINGS_KEYPATH).toStringList();
bool onlyEditorsAreRezzers = valueOrDefaultValueForKeyPath(EDITORS_ARE_REZZERS_KEYPATH).toBool();
_standardAgentPermissions[NodePermissions::standardNameLocalhost].reset(
new NodePermissions(NodePermissions::standardNameLocalhost));
_standardAgentPermissions[NodePermissions::standardNameLocalhost]->setAll(true);
_standardAgentPermissions[NodePermissions::standardNameAnonymous].reset(
new NodePermissions(NodePermissions::standardNameAnonymous));
_standardAgentPermissions[NodePermissions::standardNameLoggedIn].reset(
new NodePermissions(NodePermissions::standardNameLoggedIn));
if (isRestrictedAccess) {
// only users in allow-users list can connect
_standardAgentPermissions[NodePermissions::standardNameAnonymous]->canConnectToDomain = false;
_standardAgentPermissions[NodePermissions::standardNameLoggedIn]->canConnectToDomain = false;
} // else anonymous and logged-in retain default of canConnectToDomain = true
foreach (QString allowedUser, allowedUsers) {
// even if isRestrictedAccess is false, we have to add explicit rows for these users.
// defaults to canConnectToDomain = true
_agentPermissions[allowedUser].reset(new NodePermissions(allowedUser));
}
foreach (QString allowedEditor, allowedEditors) {
if (!_agentPermissions.contains(allowedEditor)) {
_agentPermissions[allowedEditor].reset(new NodePermissions(allowedEditor));
if (isRestrictedAccess) {
// they can change locks, but can't connect.
_agentPermissions[allowedEditor]->canConnectToDomain = false;
}
}
_agentPermissions[allowedEditor]->canAdjustLocks = true;
}
QList<QHash<QString, NodePermissionsPointer>> permissionsSets;
permissionsSets << _standardAgentPermissions << _agentPermissions;
foreach (auto permissionsSet, permissionsSets) {
foreach (QString userName, permissionsSet.keys()) {
if (onlyEditorsAreRezzers) {
permissionsSet[userName]->canRezPermanentEntities = permissionsSet[userName]->canAdjustLocks;
permissionsSet[userName]->canRezTemporaryEntities = permissionsSet[userName]->canAdjustLocks;
} else {
permissionsSet[userName]->canRezPermanentEntities = true;
permissionsSet[userName]->canRezTemporaryEntities = true;
}
}
}
packPermissions();
_standardAgentPermissions.clear();
_agentPermissions.clear();
}
}
unpackPermissions();
// write the current description version to our settings
appSettings.setValue(JSON_SETTINGS_VERSION_KEY, _descriptionVersion);
}
void DomainServerSettingsManager::packPermissionsForMap(QString mapName,
QHash<QString, NodePermissionsPointer> agentPermissions,
QString keyPath) {
QVariant* security = valueForKeyPath(_configMap.getUserConfig(), "security");
if (!security || !security->canConvert(QMetaType::QVariantMap)) {
security = valueForKeyPath(_configMap.getUserConfig(), "security", true);
(*security) = QVariantMap();
}
// save settings for anonymous / logged-in / localhost
QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), keyPath);
if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) {
permissions = valueForKeyPath(_configMap.getUserConfig(), keyPath, true);
(*permissions) = QVariantList();
}
QVariantList* permissionsList = reinterpret_cast<QVariantList*>(permissions);
(*permissionsList).clear();
foreach (QString userName, agentPermissions.keys()) {
*permissionsList += agentPermissions[userName]->toVariant();
}
}
void DomainServerSettingsManager::packPermissions() {
// transfer details from _agentPermissions to _configMap
packPermissionsForMap("standard_permissions", _standardAgentPermissions, AGENT_STANDARD_PERMISSIONS_KEYPATH);
// save settings for specific users
packPermissionsForMap("permissions", _agentPermissions, AGENT_PERMISSIONS_KEYPATH);
persistToFile();
_configMap.loadMasterAndUserConfig(_argumentList);
}
void DomainServerSettingsManager::unpackPermissions() {
// transfer details from _configMap to _agentPermissions;
_standardAgentPermissions.clear();
_agentPermissions.clear();
bool foundLocalhost = false;
bool foundAnonymous = false;
bool foundLoggedIn = false;
bool needPack = false;
QVariant* standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH);
if (!standardPermissions || !standardPermissions->canConvert(QMetaType::QVariantList)) {
qDebug() << "failed to extract standard permissions from settings.";
standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH, true);
(*standardPermissions) = QVariantList();
}
QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH);
if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) {
qDebug() << "failed to extract permissions from settings.";
permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH, true);
(*permissions) = QVariantList();
}
QList<QVariant> standardPermissionsList = standardPermissions->toList();
foreach (QVariant permsHash, standardPermissionsList) {
NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) };
QString id = perms->getID();
foundLocalhost |= (id == NodePermissions::standardNameLocalhost);
foundAnonymous |= (id == NodePermissions::standardNameAnonymous);
foundLoggedIn |= (id == NodePermissions::standardNameLoggedIn);
if (_standardAgentPermissions.contains(id)) {
qDebug() << "duplicate name in standard permissions table: " << id;
_standardAgentPermissions[id] |= perms;
needPack = true;
} else {
_standardAgentPermissions[id] = perms;
}
}
QList<QVariant> permissionsList = permissions->toList();
foreach (QVariant permsHash, permissionsList) {
NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) };
QString id = perms->getID();
if (_agentPermissions.contains(id)) {
qDebug() << "duplicate name in permissions table: " << id;
_agentPermissions[id] |= perms;
needPack = true;
} else {
_agentPermissions[id] = perms;
}
}
// if any of the standard names are missing, add them
if (!foundLocalhost) {
NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLocalhost) };
perms->setAll(true);
_standardAgentPermissions[perms->getID()] = perms;
needPack = true;
}
if (!foundAnonymous) {
NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameAnonymous) };
_standardAgentPermissions[perms->getID()] = perms;
needPack = true;
}
if (!foundLoggedIn) {
NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLoggedIn) };
_standardAgentPermissions[perms->getID()] = perms;
needPack = true;
}
if (needPack) {
packPermissions();
}
#ifdef WANT_DEBUG
qDebug() << "--------------- permissions ---------------------";
QList<QHash<QString, NodePermissionsPointer>> permissionsSets;
permissionsSets << _standardAgentPermissions << _agentPermissions;
foreach (auto permissionSet, permissionsSets) {
QHashIterator<QString, NodePermissionsPointer> i(permissionSet);
while (i.hasNext()) {
i.next();
NodePermissionsPointer perms = i.value();
qDebug() << i.key() << perms;
}
}
#endif
}
NodePermissions DomainServerSettingsManager::getStandardPermissionsForName(const QString& name) const {
if (_standardAgentPermissions.contains(name)) {
return *(_standardAgentPermissions[name].get());
}
NodePermissions nullPermissions;
nullPermissions.setAll(false);
return nullPermissions;
}
NodePermissions DomainServerSettingsManager::getPermissionsForName(const QString& name) const {
if (_agentPermissions.contains(name)) {
return *(_agentPermissions[name].get());
}
NodePermissions nullPermissions;
nullPermissions.setAll(false);
return nullPermissions;
}
QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString& keyPath) {
const QVariant* foundValue = valueForKeyPath(_configMap.getMergedConfig(), keyPath);
@ -257,7 +470,7 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
qDebug() << "DomainServerSettingsManager postedObject -" << postedObject;
// we recurse one level deep below each group for the appropriate setting
recurseJSONObjectAndOverwriteSettings(postedObject);
bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject);
// store whatever the current _settingsMap is to file
persistToFile();
@ -267,8 +480,13 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json");
// defer a restart to the domain-server, this gives our HTTPConnection enough time to respond
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
if (restartRequired) {
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
} else {
unpackPermissions();
emit updateNodePermissions();
}
return true;
} else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH_JSON) {
@ -282,7 +500,6 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
rootObject[SETTINGS_RESPONSE_VALUE_KEY] = responseObjectForType("", true);
rootObject[SETTINGS_RESPONSE_LOCKED_VALUES_KEY] = QJsonDocument::fromVariant(_configMap.getMasterConfig()).object();
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json");
}
@ -458,6 +675,8 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV
// TODO: we still need to recurse here with the description in case values in the array have special types
settingMap[key] = newValue.toArray().toVariantList();
}
sortPermissions();
}
QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName) {
@ -471,9 +690,10 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson
return QJsonObject();
}
void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) {
bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) {
auto& settingsVariant = _configMap.getUserConfig();
bool needRestart = false;
// Iterate on the setting groups
foreach(const QString& rootKey, postedObject.keys()) {
QJsonValue rootValue = postedObject[rootKey];
@ -521,6 +741,9 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject);
if (rootKey != "security") {
needRestart = true;
}
} else {
qDebug() << "Setting for root key" << rootKey << "does not exist - cannot update setting.";
}
@ -534,6 +757,9 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
QJsonValue settingValue = rootValue.toObject()[settingKey];
updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject);
if (rootKey != "security") {
needRestart = true;
}
} else {
qDebug() << "Could not find description for setting" << settingKey << "in group" << rootKey <<
"- cannot update setting.";
@ -549,9 +775,42 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
// re-merge the user and master configs after a settings change
_configMap.mergeMasterAndUserConfigs();
return needRestart;
}
// Compare two members of a permissions list
bool permissionVariantLessThan(const QVariant &v1, const QVariant &v2) {
if (!v1.canConvert(QMetaType::QVariantMap) ||
!v2.canConvert(QMetaType::QVariantMap)) {
return v1.toString() < v2.toString();
}
QVariantMap m1 = v1.toMap();
QVariantMap m2 = v2.toMap();
if (!m1.contains("permissions_id") ||
!m2.contains("permissions_id")) {
return v1.toString() < v2.toString();
}
return m1["permissions_id"].toString() < m2["permissions_id"].toString();
}
void DomainServerSettingsManager::sortPermissions() {
// sort the permission-names
QVariant* standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH);
if (standardPermissions && standardPermissions->canConvert(QMetaType::QVariantList)) {
QList<QVariant>* standardPermissionsList = reinterpret_cast<QVariantList*>(standardPermissions);
std::sort((*standardPermissionsList).begin(), (*standardPermissionsList).end(), permissionVariantLessThan);
}
QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH);
if (permissions && permissions->canConvert(QMetaType::QVariantList)) {
QList<QVariant>* permissionsList = reinterpret_cast<QVariantList*>(permissions);
std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan);
}
}
void DomainServerSettingsManager::persistToFile() {
sortPermissions();
// make sure we have the dir the settings file is supposed to live in
QFileInfo settingsFileInfo(_configMap.getUserConfigFilename());

View file

@ -19,14 +19,14 @@
#include <HTTPManager.h>
#include <ReceivedMessage.h>
#include "NodePermissions.h"
const QString SETTINGS_PATHS_KEY = "paths";
const QString SETTINGS_PATH = "/settings";
const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json";
const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users";
const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access";
const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions";
const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions";
class DomainServerSettingsManager : public QObject {
Q_OBJECT
@ -41,16 +41,29 @@ public:
QVariantMap& getUserSettingsMap() { return _configMap.getUserConfig(); }
QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); }
bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name); }
bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name); }
NodePermissions getStandardPermissionsForName(const QString& name) const;
NodePermissions getPermissionsForName(const QString& name) const;
QStringList getAllNames() { return _agentPermissions.keys(); }
signals:
void updateNodePermissions();
private slots:
void processSettingsRequestPacket(QSharedPointer<ReceivedMessage> message);
private:
QStringList _argumentList;
QJsonObject responseObjectForType(const QString& typeValue, bool isAuthenticated = false);
void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject);
bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject);
void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap,
const QJsonObject& settingDescription);
QJsonObject settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName);
void sortPermissions();
void persistToFile();
double _descriptionVersion;
@ -58,6 +71,12 @@ private:
HifiConfigVariantMap _configMap;
friend class DomainServer;
void packPermissionsForMap(QString mapName, QHash<QString, NodePermissionsPointer> agentPermissions, QString keyPath);
void packPermissions();
void unpackPermissions();
QHash<QString, NodePermissionsPointer> _standardAgentPermissions; // anonymous, logged-in, localhost
QHash<QString, NodePermissionsPointer> _agentPermissions; // specific account-names
};
#endif // hifi_DomainServerSettingsManager_h

View file

@ -199,7 +199,7 @@ FocusScope {
anchors.leftMargin: hifi.dimensions.textPadding
anchors.verticalCenter: parent.verticalCenter
id: popupText
text: listView.model[index]
text: listView.model[index] ? listView.model[index] : ""
size: hifi.fontSizes.textFieldInput
color: hifi.colors.baseGray
}

View file

@ -186,7 +186,12 @@ ModalWindow {
}
if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) {
if (root.selectDirectory) {
currentSelection.text = currentText !== "This PC" ? currentText : "";
d.currentSelectionUrl = helper.pathToUrl(currentText);
}
fileTableModel.folder = folder;
fileTableView.forceActiveFocus();
}
}
}
@ -212,9 +217,11 @@ ModalWindow {
function update() {
var row = fileTableView.currentRow;
openButton.text = root.selectDirectory && row === -1 ? "Choose" : "Open"
if (row === -1) {
if (!root.selectDirectory) {
currentSelection.text = "";
currentSelectionIsFolder = false;
}
return;
}
@ -445,12 +452,6 @@ ModalWindow {
onSortIndicatorOrderChanged: { updateSort(); }
onActiveFocusChanged: {
if (activeFocus && currentRow == -1) {
fileTableView.selection.select(0)
}
}
itemDelegate: Item {
clip: true
@ -607,6 +608,12 @@ ModalWindow {
readOnly: !root.saveDialog
activeFocusOnTab: !readOnly
onActiveFocusChanged: if (activeFocus) { selectAll(); }
onTextChanged: {
if (root.saveDialog && text !== "") {
fileTableView.selection.clear();
fileTableView.currentRow = -1;
}
}
onAccepted: okAction.trigger();
}
@ -652,7 +659,7 @@ ModalWindow {
Action {
id: okAction
text: root.saveDialog ? "Save" : (root.selectDirectory ? "Choose" : "Open")
text: currentSelection.text ? (root.selectDirectory && fileTableView.currentRow === -1 ? "Choose" : (root.saveDialog ? "Save" : "Open")) : "Open"
enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false
onTriggered: {
if (!root.selectDirectory && !d.currentSelectionIsFolder
@ -676,7 +683,6 @@ ModalWindow {
return;
}
// Handle the ambiguity between different cases
// * typed name (with or without extension)
// * full path vs relative vs filename only

View file

@ -630,7 +630,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails()));
connect(&domainHandler, &DomainHandler::resetting, nodeList.data(), &NodeList::resetDomainServerCheckInVersion);
connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused);
// update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one
@ -654,9 +653,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated);
connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID);
connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID);
connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, this, &Application::limitOfSilentDomainCheckInsReached);
connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch);
// you might think we could just do this in NodeList but we only want this connection for Interface
connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, nodeList.data(), &NodeList::reset);
// connect to appropriate slots on AccountManager
auto accountManager = DependencyManager::get<AccountManager>();
@ -1074,8 +1075,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCode) {
switch (static_cast<DomainHandler::ConnectionRefusedReason>(reasonCode)) {
case DomainHandler::ConnectionRefusedReason::ProtocolMismatch:
notifyPacketVersionMismatch();
break;
case DomainHandler::ConnectionRefusedReason::TooManyUsers:
case DomainHandler::ConnectionRefusedReason::Unknown: {
QString message = "Unable to connect to the location you are visiting.\n";
@ -4294,7 +4293,7 @@ void Application::nodeActivated(SharedNodePointer node) {
if (assetDialog) {
auto nodeList = DependencyManager::get<NodeList>();
if (nodeList->getThisNodeCanRez()) {
if (nodeList->getThisNodeCanWriteAssets()) {
// call reload on the shown asset browser dialog to get the mappings (if permissions allow)
QMetaObject::invokeMethod(assetDialog, "reload");
} else {
@ -4620,17 +4619,6 @@ void Application::setSessionUUID(const QUuid& sessionUUID) const {
Physics::setSessionUUID(sessionUUID);
}
// If we're not getting anything back from the domain server checkin, it might be that the domain speaks an
// older version of the DomainConnectRequest protocol. We will attempt to send and older version of DomainConnectRequest.
// We won't actually complete the connection, but if the server responds, we know that it needs to be upgraded (or we
// need to be downgraded to talk to it).
void Application::limitOfSilentDomainCheckInsReached() {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->downgradeDomainServerCheckInVersion(); // attempt to use an older domain checkin version
nodeList->reset();
}
bool Application::askToSetAvatarUrl(const QString& url) {
QUrl realUrl(url);
if (realUrl.isLocalFile()) {
@ -4801,7 +4789,7 @@ void Application::toggleRunningScriptsWidget() const {
}
void Application::toggleAssetServerWidget(QString filePath) {
if (!DependencyManager::get<NodeList>()->getThisNodeCanRez()) {
if (!DependencyManager::get<NodeList>()->getThisNodeCanWriteAssets()) {
return;
}

View file

@ -318,7 +318,6 @@ private slots:
bool displayAvatarAttachmentConfirmationDialog(const QString& name) const;
void setSessionUUID(const QUuid& sessionUUID) const;
void limitOfSilentDomainCheckInsReached();
void domainChanged(const QString& domainHostname);
void updateWindowTitle() const;

View file

@ -136,8 +136,8 @@ Menu::Menu() {
Qt::CTRL | Qt::SHIFT | Qt::Key_A,
qApp, SLOT(toggleAssetServerWidget()));
auto nodeList = DependencyManager::get<NodeList>();
QObject::connect(nodeList.data(), &NodeList::canRezChanged, assetServerAction, &QAction::setEnabled);
assetServerAction->setEnabled(nodeList->getThisNodeCanRez());
QObject::connect(nodeList.data(), &NodeList::canWriteAssetsChanged, assetServerAction, &QAction::setEnabled);
assetServerAction->setEnabled(nodeList->getThisNodeCanWriteAssets());
// Edit > Package Model... [advanced]
addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0,
@ -542,6 +542,9 @@ Menu::Menu() {
#if (PR_BUILD || DEV_BUILD)
addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::SendWrongProtocolVersion, 0, false,
qApp, SLOT(sendWrongProtocolVersionsSignature(bool)));
addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::SendWrongDSConnectVersion, 0, false,
nodeList.data(), SLOT(toggleSendNewerDSConnectVersion(bool)));
#endif

View file

@ -166,6 +166,7 @@ namespace MenuOption {
const QString RunTimingTests = "Run Timing Tests";
const QString ScriptEditor = "Script Editor...";
const QString ScriptedMotorControl = "Enable Scripted Motor Control";
const QString SendWrongDSConnectVersion = "Send wrong DS connect version";
const QString SendWrongProtocolVersion = "Send wrong protocol version";
const QString SetHomeLocation = "Set Home Location";
const QString ShowDSConnectTable = "Show Domain Connection Timing";

View file

@ -32,6 +32,7 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership
auto nodeList = DependencyManager::get<NodeList>();
connect(nodeList.data(), &NodeList::isAllowedEditorChanged, this, &EntityScriptingInterface::canAdjustLocksChanged);
connect(nodeList.data(), &NodeList::canRezChanged, this, &EntityScriptingInterface::canRezChanged);
connect(nodeList.data(), &NodeList::canRezTmpChanged, this, &EntityScriptingInterface::canRezTmpChanged);
}
void EntityScriptingInterface::queueEntityMessage(PacketType packetType,
@ -49,6 +50,11 @@ bool EntityScriptingInterface::canRez() {
return nodeList->getThisNodeCanRez();
}
bool EntityScriptingInterface::canRezTmp() {
auto nodeList = DependencyManager::get<NodeList>();
return nodeList->getThisNodeCanRezTmp();
}
void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) {
if (_entityTree) {
disconnect(_entityTree.get(), &EntityTree::addingEntity, this, &EntityScriptingInterface::addingEntity);

View file

@ -80,6 +80,7 @@ public slots:
// returns true if the DomainServer will allow this Node/Avatar to rez new entities
Q_INVOKABLE bool canRez();
Q_INVOKABLE bool canRezTmp();
/// adds a model with the specific properties
Q_INVOKABLE QUuid addEntity(const EntityItemProperties& properties, bool clientOnly = false);
@ -179,6 +180,7 @@ signals:
void canAdjustLocksChanged(bool canAdjustLocks);
void canRezChanged(bool canRez);
void canRezTmpChanged(bool canRez);
void mousePressOnEntity(const EntityItemID& entityItemID, const MouseEvent& event);
void mouseMoveOnEntity(const EntityItemID& entityItemID, const MouseEvent& event);

View file

@ -26,6 +26,8 @@
#include "LogHandler.h"
static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC * 50;
const float EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME = 60 * 60; // 1 hour
EntityTree::EntityTree(bool shouldReaverage) :
Octree(shouldReaverage),
@ -128,13 +130,16 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI
EntityItemProperties properties = origProperties;
bool allowLockChange;
bool canRezPermanentEntities;
QUuid senderID;
if (senderNode.isNull()) {
auto nodeList = DependencyManager::get<NodeList>();
allowLockChange = nodeList->isAllowedEditor();
canRezPermanentEntities = nodeList->getThisNodeCanRez();
senderID = nodeList->getSessionUUID();
} else {
allowLockChange = senderNode->isAllowedEditor();
canRezPermanentEntities = senderNode->getCanRez();
senderID = senderNode->getUUID();
}
@ -143,6 +148,12 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI
return false;
}
if (!canRezPermanentEntities && (entity->getLifetime() != properties.getLifetime())) {
// we don't allow a Node that can't create permanent entities to adjust lifetimes on existing ones
qCDebug(entities) << "Refusing disallowed entity lifetime adjustment.";
return false;
}
// enforce support for locked entities. If an entity is currently locked, then the only
// property we allow you to change is the locked property.
if (entity->getLocked()) {
@ -308,17 +319,39 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI
return true;
}
bool EntityTree::permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp) {
float lifeTime = properties.getLifetime();
if (lifeTime == 0.0f || lifeTime > _maxTmpEntityLifetime) {
// this is an attempt to rez a permanent entity.
if (!canRez) {
return false;
}
} else {
// this is an attempt to rez a temporary entity.
if (!canRezTmp) {
return false;
}
}
return true;
}
EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) {
EntityItemPointer result = NULL;
auto nodeList = DependencyManager::get<NodeList>();
if (!nodeList) {
qDebug() << "EntityTree::addEntity -- can't get NodeList";
return nullptr;
}
bool clientOnly = properties.getClientOnly();
if (!clientOnly && getIsClient()) {
if (!clientOnly && getIsClient() &&
!permissionsAllowRez(properties, nodeList->getThisNodeCanRez(), nodeList->getThisNodeCanRezTmp())) {
// if our Node isn't allowed to create entities in this domain, don't try.
auto nodeList = DependencyManager::get<NodeList>();
if (nodeList && !nodeList->getThisNodeCanRez()) {
return NULL;
}
return nullptr;
}
bool recordCreationTime = false;
@ -920,7 +953,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
endUpdate = usecTimestampNow();
_totalUpdates++;
} else if (message.getType() == PacketType::EntityAdd) {
if (senderNode->getCanRez()) {
if (permissionsAllowRez(properties, senderNode->getCanRez(), senderNode->getCanRezTmp())) {
// this is a new entity... assign a new entityID
properties.setCreated(properties.getLastEdited());
startCreate = usecTimestampNow();
@ -1430,6 +1463,12 @@ bool EntityTree::readFromMap(QVariantMap& map) {
QVariantList entitiesQList = map["Entities"].toList();
QScriptEngine scriptEngine;
if (entitiesQList.length() == 0) {
// Empty map or invalidly formed file.
return false;
}
bool success = true;
foreach (QVariant entityVariant, entitiesQList) {
// QVariantMap --> QScriptValue --> EntityItemProperties --> Entity
QVariantMap entityMap = entityVariant.toMap();
@ -1447,9 +1486,10 @@ bool EntityTree::readFromMap(QVariantMap& map) {
EntityItemPointer entity = addEntity(entityItemID, properties);
if (!entity) {
qCDebug(entities) << "adding Entity failed:" << entityItemID << properties.getType();
success = false;
}
}
return true;
return success;
}
void EntityTree::resetClientEditStats() {

View file

@ -62,6 +62,10 @@ public:
void createRootElement();
void setEntityMaxTmpLifetime(float maxTmpEntityLifetime) { _maxTmpEntityLifetime = maxTmpEntityLifetime; }
bool permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp);
/// Implements our type specific root element factory
virtual OctreeElementPointer createNewElement(unsigned char* octalCode = NULL) override;
@ -252,6 +256,8 @@ public:
void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID);
static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME;
public slots:
void callLoader(EntityItemID entityID);
@ -331,6 +337,8 @@ protected:
// we maintain a list of avatarIDs to notice when an entity is a child of one.
QSet<QUuid> _avatarIDs; // IDs of avatars connected to entity server
QHash<QUuid, QSet<EntityItemID>> _childrenOfAvatars; // which entities are children of which avatars
float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME };
};
#endif // hifi_EntityTree_h

View file

@ -144,12 +144,21 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) {
// 4. domain network address (IP or dns resolvable hostname)
// use our regex'ed helpers to figure out what we're supposed to do with this
if (!handleUsername(lookupUrl.authority())) {
if (handleUsername(lookupUrl.authority())) {
// handled a username for lookup
// in case we're failing to connect to where we thought this user was
// store their username as previous lookup so we can refresh their location via API
_previousLookup = lookupUrl;
} else {
// we're assuming this is either a network address or global place name
// check if it is a network address first
bool hostChanged;
if (handleNetworkAddress(lookupUrl.host()
+ (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) {
+ (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) {
// a network address lookup clears the previous lookup since we don't expect to re-attempt it
_previousLookup.clear();
// If the host changed then we have already saved to history
if (hostChanged) {
@ -165,10 +174,16 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) {
// we may have a path that defines a relative viewpoint - if so we should jump to that now
handlePath(path, trigger);
} else if (handleDomainID(lookupUrl.host())){
// store this domain ID as the previous lookup in case we're failing to connect and want to refresh API info
_previousLookup = lookupUrl;
// no place name - this is probably a domain ID
// try to look up the domain ID on the metaverse API
attemptDomainIDLookup(lookupUrl.host(), lookupUrl.path(), trigger);
} else {
// store this place name as the previous lookup in case we fail to connect and want to refresh API info
_previousLookup = lookupUrl;
// wasn't an address - lookup the place name
// we may have a path that defines a relative viewpoint - pass that through the lookup so we can go to it after
attemptPlaceNameLookup(lookupUrl.host(), lookupUrl.path(), trigger);
@ -180,9 +195,13 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) {
} else if (lookupUrl.toString().startsWith('/')) {
qCDebug(networking) << "Going to relative path" << lookupUrl.path();
// a path lookup clears the previous lookup since we don't expect to re-attempt it
_previousLookup.clear();
// if this is a relative path then handle it as a relative viewpoint
handlePath(lookupUrl.path(), trigger, true);
emit lookupResultsFinished();
return true;
}
@ -276,7 +295,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const
qCDebug(networking) << "Possible domain change required to connect to" << domainHostname
<< "on" << domainPort;
emit possibleDomainChangeRequired(domainHostname, domainPort);
emit possibleDomainChangeRequired(domainHostname, domainPort, domainID);
} else {
QString iceServerAddress = domainObject[DOMAIN_ICE_SERVER_ADDRESS_KEY].toString();
@ -315,7 +334,10 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const
QString overridePath = reply.property(OVERRIDE_PATH_KEY).toString();
if (!overridePath.isEmpty()) {
handlePath(overridePath, trigger);
// make sure we don't re-handle an overriden path if this was a refresh of info from API
if (trigger != LookupTrigger::AttemptedRefresh) {
handlePath(overridePath, trigger);
}
} else {
// take the path that came back
const QString PLACE_PATH_KEY = "path";
@ -362,7 +384,7 @@ void AddressManager::handleAPIError(QNetworkReply& errorReply) {
if (errorReply.error() == QNetworkReply::ContentNotFoundError) {
// if this is a lookup that has no result, don't keep re-trying it
//_previousLookup.clear();
_previousLookup.clear();
emit lookupResultIsNotFound();
}
@ -598,7 +620,7 @@ bool AddressManager::setDomainInfo(const QString& hostname, quint16 port, Lookup
DependencyManager::get<NodeList>()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::HandleAddress);
emit possibleDomainChangeRequired(hostname, port);
emit possibleDomainChangeRequired(hostname, port, QUuid());
return hostChanged;
}
@ -618,6 +640,13 @@ void AddressManager::goToUser(const QString& username) {
QByteArray(), nullptr, requestParams);
}
void AddressManager::refreshPreviousLookup() {
// if we have a non-empty previous lookup, fire it again now (but don't re-store it in the history)
if (!_previousLookup.isEmpty()) {
handleUrl(_previousLookup, LookupTrigger::AttemptedRefresh);
}
}
void AddressManager::copyAddress() {
QApplication::clipboard()->setText(currentAddress().toString());
}
@ -629,7 +658,10 @@ void AddressManager::copyPath() {
void AddressManager::addCurrentAddressToHistory(LookupTrigger trigger) {
// if we're cold starting and this is called for the first address (from settings) we don't do anything
if (trigger != LookupTrigger::StartupFromSettings && trigger != LookupTrigger::DomainPathResponse) {
if (trigger != LookupTrigger::StartupFromSettings
&& trigger != LookupTrigger::DomainPathResponse
&& trigger != LookupTrigger::AttemptedRefresh) {
if (trigger == LookupTrigger::Back) {
// we're about to push to the forward stack
// if it's currently empty emit our signal to say that going forward is now possible

View file

@ -48,7 +48,8 @@ public:
Forward,
StartupFromSettings,
DomainPathResponse,
Internal
Internal,
AttemptedRefresh
};
bool isConnected();
@ -89,6 +90,8 @@ public slots:
void goToUser(const QString& username);
void refreshPreviousLookup();
void storeCurrentAddress();
void copyAddress();
@ -99,7 +102,7 @@ signals:
void lookupResultIsOffline();
void lookupResultIsNotFound();
void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort);
void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort, const QUuid& domainID);
void possibleDomainChangeRequiredViaICEForID(const QString& iceServerHostname, const QUuid& domainID);
void locationChangeRequired(const glm::vec3& newPosition,
@ -152,6 +155,8 @@ private:
quint64 _lastBackPush = 0;
QString _newHostLookupPath;
QUrl _previousLookup;
};
#endif // hifi_AddressManager_h

View file

@ -14,6 +14,7 @@
#include <QtCore/QJsonDocument>
#include <QtCore/QDataStream>
#include "AddressManager.h"
#include "Assignment.h"
#include "HifiSockAddr.h"
#include "NodeList.h"
@ -28,17 +29,10 @@
DomainHandler::DomainHandler(QObject* parent) :
QObject(parent),
_uuid(),
_sockAddr(HifiSockAddr(QHostAddress::Null, DEFAULT_DOMAIN_SERVER_PORT)),
_assignmentUUID(),
_connectionToken(),
_iceDomainID(),
_iceClientID(),
_iceServerSockAddr(),
_icePeer(this),
_isConnected(false),
_settingsObject(),
_settingsTimer(this)
_settingsTimer(this),
_apiRefreshTimer(this)
{
_sockAddr.setObjectName("DomainServer");
@ -49,6 +43,16 @@ DomainHandler::DomainHandler(QObject* parent) :
static const int DOMAIN_SETTINGS_TIMEOUT_MS = 5000;
_settingsTimer.setInterval(DOMAIN_SETTINGS_TIMEOUT_MS);
connect(&_settingsTimer, &QTimer::timeout, this, &DomainHandler::settingsReceiveFail);
// setup the API refresh timer for auto connection information refresh from API when failing to connect
const int API_REFRESH_TIMEOUT_MSEC = 2500;
_apiRefreshTimer.setInterval(API_REFRESH_TIMEOUT_MSEC);
auto addressManager = DependencyManager::get<AddressManager>();
connect(&_apiRefreshTimer, &QTimer::timeout, addressManager.data(), &AddressManager::refreshPreviousLookup);
// stop the refresh timer if we connect to a domain
connect(this, &DomainHandler::connectedToDomain, &_apiRefreshTimer, &QTimer::stop);
}
void DomainHandler::disconnect() {
@ -93,10 +97,14 @@ void DomainHandler::softReset() {
clearSettings();
_domainConnectionRefusals.clear();
_connectionDenialsSinceKeypairRegen = 0;
// cancel the failure timeout for any pending requests for settings
QMetaObject::invokeMethod(&_settingsTimer, "stop");
// restart the API refresh timer in case we fail to connect and need to refresh information
QMetaObject::invokeMethod(&_apiRefreshTimer, "start");
}
void DomainHandler::hardReset() {
@ -105,7 +113,7 @@ void DomainHandler::hardReset() {
softReset();
qCDebug(networking) << "Hard reset in NodeList DomainHandler.";
_iceDomainID = QUuid();
_pendingDomainID = QUuid();
_iceServerSockAddr = HifiSockAddr();
_hostname = QString();
_sockAddr.clear();
@ -139,7 +147,9 @@ void DomainHandler::setUUID(const QUuid& uuid) {
}
}
void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) {
void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const QUuid& domainID) {
_pendingDomainID = domainID;
if (hostname != _hostname || _sockAddr.getPort() != port) {
// re-set the domain info so that auth information is reloaded
@ -149,9 +159,6 @@ void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) {
// set the new hostname
_hostname = hostname;
// FIXME - is this the right place???
_domainConnectionRefusals.clear();
qCDebug(networking) << "Updated domain hostname to" << _hostname;
// re-set the sock addr to null and fire off a lookup of the IP address for this domain-server's hostname
@ -174,14 +181,15 @@ void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) {
}
void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id) {
if (id != _uuid) {
if (_iceServerSockAddr.getAddress().toString() != iceServerHostname || id != _pendingDomainID) {
// re-set the domain info to connect to new domain
hardReset();
// refresh our ICE client UUID to something new
_iceClientID = QUuid::createUuid();
_iceDomainID = id;
_pendingDomainID = id;
HifiSockAddr* replaceableSockAddr = &_iceServerSockAddr;
replaceableSockAddr->~HifiSockAddr();
@ -255,6 +263,7 @@ void DomainHandler::setIsConnected(bool isConnected) {
// we've connected to new domain - time to ask it for global settings
requestDomainSettings();
} else {
emit disconnectedFromDomain();
}
@ -305,6 +314,9 @@ void DomainHandler::processICEPingReplyPacket(QSharedPointer<ReceivedMessage> me
qCDebug(networking) << "Received reply from domain-server on" << senderSockAddr;
if (getIP().isNull()) {
// we're hearing back from this domain-server, no need to refresh API information
_apiRefreshTimer.stop();
// for now we're unsafely assuming this came back from the domain
if (senderSockAddr == _icePeer.getLocalSocket()) {
qCDebug(networking) << "Connecting to domain using local socket";
@ -333,17 +345,20 @@ void DomainHandler::processDTLSRequirementPacket(QSharedPointer<ReceivedMessage>
void DomainHandler::processICEResponsePacket(QSharedPointer<ReceivedMessage> message) {
if (_icePeer.hasSockets()) {
qDebug() << "Received an ICE peer packet for domain-server but we already have sockets. Not processing.";
// bail on processing this packet if our ice peer doesn't have sockets
// bail on processing this packet if our ice peer already has sockets
return;
}
// start or restart the API refresh timer now that we have new information
_apiRefreshTimer.start();
QDataStream iceResponseStream(message->getMessage());
iceResponseStream >> _icePeer;
DependencyManager::get<NodeList>()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSPeerInformation);
if (_icePeer.getUUID() != _iceDomainID) {
if (_icePeer.getUUID() != _pendingDomainID) {
qCDebug(networking) << "Received a network peer with ID that does not match current domain. Will not attempt connection.";
_icePeer.reset();
} else {
@ -373,6 +388,9 @@ bool DomainHandler::reasonSuggestsLogin(ConnectionRefusedReason reasonCode) {
}
void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer<ReceivedMessage> message) {
// we're hearing from this domain-server, don't need to refresh API info
_apiRefreshTimer.stop();
// Read deny reason from packet
uint8_t reasonCodeWire;

View file

@ -58,8 +58,8 @@ public:
const QUuid& getAssignmentUUID() const { return _assignmentUUID; }
void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; }
const QUuid& getICEDomainID() const { return _iceDomainID; }
const QUuid& getPendingDomainID() const { return _pendingDomainID; }
const QUuid& getICEClientID() const { return _iceClientID; }
@ -75,7 +75,6 @@ public:
bool hasSettings() const { return !_settingsObject.isEmpty(); }
void requestDomainSettings();
const QJsonObject& getSettingsObject() const { return _settingsObject; }
void setPendingPath(const QString& pendingPath) { _pendingPath = pendingPath; }
const QString& getPendingPath() { return _pendingPath; }
@ -94,7 +93,7 @@ public:
};
public slots:
void setHostnameAndPort(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT);
void setSocketAndID(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT, const QUuid& id = QUuid());
void setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id);
void processSettingsPacketList(QSharedPointer<ReceivedMessage> packetList);
@ -136,11 +135,11 @@ private:
HifiSockAddr _sockAddr;
QUuid _assignmentUUID;
QUuid _connectionToken;
QUuid _iceDomainID;
QUuid _pendingDomainID; // ID of domain being connected to, via ICE or direct connection
QUuid _iceClientID;
HifiSockAddr _iceServerSockAddr;
NetworkPeer _icePeer;
bool _isConnected;
bool _isConnected { false };
QJsonObject _settingsObject;
QString _pendingPath;
QTimer _settingsTimer;
@ -148,6 +147,8 @@ private:
QStringList _domainConnectionRefusals;
bool _hasCheckedForAccessToken { false };
int _connectionDenialsSinceKeypairRegen { 0 };
QTimer _apiRefreshTimer;
};
#endif // hifi_DomainHandler_h

View file

@ -52,7 +52,7 @@ LimitedNodeList::LimitedNodeList(unsigned short socketListenPort, unsigned short
_numCollectedPackets(0),
_numCollectedBytes(0),
_packetStatTimer(),
_thisNodeCanRez(true)
_permissions(NodePermissions())
{
static bool firstCall = true;
if (firstCall) {
@ -130,17 +130,22 @@ void LimitedNodeList::setSessionUUID(const QUuid& sessionUUID) {
}
}
void LimitedNodeList::setIsAllowedEditor(bool isAllowedEditor) {
if (_isAllowedEditor != isAllowedEditor) {
_isAllowedEditor = isAllowedEditor;
emit isAllowedEditorChanged(isAllowedEditor);
}
}
void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) {
NodePermissions originalPermissions = _permissions;
void LimitedNodeList::setThisNodeCanRez(bool canRez) {
if (_thisNodeCanRez != canRez) {
_thisNodeCanRez = canRez;
emit canRezChanged(canRez);
_permissions = newPermissions;
if (originalPermissions.canAdjustLocks != newPermissions.canAdjustLocks) {
emit isAllowedEditorChanged(_permissions.canAdjustLocks);
}
if (originalPermissions.canRezPermanentEntities != newPermissions.canRezPermanentEntities) {
emit canRezChanged(_permissions.canRezPermanentEntities);
}
if (originalPermissions.canRezTemporaryEntities != newPermissions.canRezTemporaryEntities) {
emit canRezTmpChanged(_permissions.canRezTemporaryEntities);
}
if (originalPermissions.canWriteToAssetServer != newPermissions.canWriteToAssetServer) {
emit canWriteAssetsChanged(_permissions.canWriteToAssetServer);
}
}
@ -515,7 +520,7 @@ void LimitedNodeList::handleNodeKill(const SharedNodePointer& node) {
SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType,
const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
bool isAllowedEditor, bool canRez,
const NodePermissions& permissions,
const QUuid& connectionSecret) {
NodeHash::const_iterator it = _nodeHash.find(uuid);
@ -524,14 +529,13 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
matchingNode->setPublicSocket(publicSocket);
matchingNode->setLocalSocket(localSocket);
matchingNode->setIsAllowedEditor(isAllowedEditor);
matchingNode->setCanRez(canRez);
matchingNode->setPermissions(permissions);
matchingNode->setConnectionSecret(connectionSecret);
return matchingNode;
} else {
// we didn't have this node, so add them
Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket, isAllowedEditor, canRez, connectionSecret, this);
Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket, permissions, connectionSecret, this);
if (nodeType == NodeType::AudioMixer) {
LimitedNodeList::flagTimeForConnectionStep(LimitedNodeList::AddedAudioMixer);

View file

@ -104,12 +104,12 @@ public:
const QUuid& getSessionUUID() const { return _sessionUUID; }
void setSessionUUID(const QUuid& sessionUUID);
bool isAllowedEditor() const { return _isAllowedEditor; }
void setIsAllowedEditor(bool isAllowedEditor);
void setPermissions(const NodePermissions& newPermissions);
bool isAllowedEditor() const { return _permissions.canAdjustLocks; }
bool getThisNodeCanRez() const { return _permissions.canRezPermanentEntities; }
bool getThisNodeCanRezTmp() const { return _permissions.canRezTemporaryEntities; }
bool getThisNodeCanWriteAssets() const { return _permissions.canWriteToAssetServer; }
bool getThisNodeCanRez() const { return _thisNodeCanRez; }
void setThisNodeCanRez(bool canRez);
quint16 getSocketLocalPort() const { return _nodeSocket.localPort(); }
QUdpSocket& getDTLSSocket();
@ -137,7 +137,7 @@ public:
SharedNodePointer addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType,
const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
bool isAllowedEditor = false, bool canRez = false,
const NodePermissions& permissions = DEFAULT_AGENT_PERMISSIONS,
const QUuid& connectionSecret = QUuid());
bool hasCompletedInitialSTUN() const { return _hasCompletedInitialSTUN; }
@ -254,6 +254,8 @@ signals:
void isAllowedEditorChanged(bool isAllowedEditor);
void canRezChanged(bool canRez);
void canRezTmpChanged(bool canRezTmp);
void canWriteAssetsChanged(bool canWriteAssets);
protected slots:
void connectedForLocalSocketTest();
@ -300,8 +302,7 @@ protected:
int _numCollectedBytes;
QElapsedTimer _packetStatTimer;
bool _isAllowedEditor { false };
bool _thisNodeCanRez;
NodePermissions _permissions;
QPointer<QTimer> _initialSTUNTimer;

View file

@ -184,6 +184,11 @@ void NLPacket::setType(PacketType type) {
writeTypeAndVersion();
}
void NLPacket::setVersion(PacketVersion version) {
_version = version;
writeTypeAndVersion();
}
void NLPacket::readType() {
_type = NLPacket::typeInHeader(*this);
}

View file

@ -65,6 +65,7 @@ public:
void setType(PacketType type);
PacketVersion getVersion() const { return _version; }
void setVersion(PacketVersion version);
const QUuid& getSourceID() const { return _sourceID; }

View file

@ -16,6 +16,7 @@
#include "Node.h"
#include "SharedUtil.h"
#include "NodePermissions.h"
#include <QtCore/QDataStream>
#include <QtCore/QDebug>
@ -47,7 +48,7 @@ const QString& NodeType::getNodeTypeName(NodeType_t nodeType) {
}
Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket,
const HifiSockAddr& localSocket, bool isAllowedEditor, bool canRez, const QUuid& connectionSecret,
const HifiSockAddr& localSocket, const NodePermissions& permissions, const QUuid& connectionSecret,
QObject* parent) :
NetworkPeer(uuid, publicSocket, localSocket, parent),
_type(type),
@ -57,8 +58,7 @@ Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket,
_clockSkewUsec(0),
_mutex(),
_clockSkewMovingPercentile(30, 0.8f), // moving 80th percentile of 30 samples
_isAllowedEditor(isAllowedEditor),
_canRez(canRez)
_permissions(permissions)
{
// Update socket's object name
setType(_type);
@ -78,15 +78,12 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) {
_clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile();
}
QDataStream& operator<<(QDataStream& out, const Node& node) {
out << node._type;
out << node._uuid;
out << node._publicSocket;
out << node._localSocket;
out << node._isAllowedEditor;
out << node._canRez;
out << node._permissions;
return out;
}
@ -95,9 +92,7 @@ QDataStream& operator>>(QDataStream& in, Node& node) {
in >> node._uuid;
in >> node._publicSocket;
in >> node._localSocket;
in >> node._isAllowedEditor;
in >> node._canRez;
in >> node._permissions;
return in;
}

View file

@ -27,13 +27,14 @@
#include "NodeType.h"
#include "SimpleMovingAverage.h"
#include "MovingPercentile.h"
#include "NodePermissions.h"
class Node : public NetworkPeer {
Q_OBJECT
public:
Node(const QUuid& uuid, NodeType_t type,
const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
bool isAllowedEditor, bool canRez, const QUuid& connectionSecret = QUuid(),
const NodePermissions& permissions, const QUuid& connectionSecret = QUuid(),
QObject* parent = 0);
bool operator==(const Node& otherNode) const { return _uuid == otherNode._uuid; }
@ -58,11 +59,12 @@ public:
void updateClockSkewUsec(qint64 clockSkewSample);
QMutex& getMutex() { return _mutex; }
void setIsAllowedEditor(bool isAllowedEditor) { _isAllowedEditor = isAllowedEditor; }
bool isAllowedEditor() { return _isAllowedEditor; }
void setCanRez(bool canRez) { _canRez = canRez; }
bool getCanRez() { return _canRez; }
void setPermissions(const NodePermissions& newPermissions) { _permissions = newPermissions; }
NodePermissions getPermissions() const { return _permissions; }
bool isAllowedEditor() const { return _permissions.canAdjustLocks; }
bool getCanRez() const { return _permissions.canRezPermanentEntities; }
bool getCanRezTmp() const { return _permissions.canRezTemporaryEntities; }
bool getCanWriteToAssetServer() const { return _permissions.canWriteToAssetServer; }
friend QDataStream& operator<<(QDataStream& out, const Node& node);
friend QDataStream& operator>>(QDataStream& in, Node& node);
@ -81,8 +83,7 @@ private:
qint64 _clockSkewUsec;
QMutex _mutex;
MovingPercentile _clockSkewMovingPercentile;
bool _isAllowedEditor;
bool _canRez;
NodePermissions _permissions;
};
Q_DECLARE_METATYPE(Node*)

View file

@ -50,7 +50,7 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned
// handle domain change signals from AddressManager
connect(addressManager.data(), &AddressManager::possibleDomainChangeRequired,
&_domainHandler, &DomainHandler::setHostnameAndPort);
&_domainHandler, &DomainHandler::setSocketAndID);
connect(addressManager.data(), &AddressManager::possibleDomainChangeRequiredViaICEForID,
&_domainHandler, &DomainHandler::setIceServerHostnameAndID);
@ -250,7 +250,6 @@ void NodeList::sendDomainServerCheckIn() {
qCDebug(networking) << "Waiting for ICE discovered domain-server socket. Will not send domain-server check in.";
handleICEConnectionToDomainServer();
} else if (!_domainHandler.getIP().isNull()) {
bool isUsingDTLS = false;
PacketType domainPacketType = !_domainHandler.isConnected()
? PacketType::DomainConnectRequest : PacketType::DomainListRequest;
@ -292,12 +291,18 @@ void NodeList::sendDomainServerCheckIn() {
return;
}
auto packetVersion = (domainPacketType == PacketType::DomainConnectRequest) ? _domainConnectRequestVersion : 0;
auto domainPacket = NLPacket::create(domainPacketType, -1, false, false, packetVersion);
auto domainPacket = NLPacket::create(domainPacketType);
QDataStream packetStream(domainPacket.get());
if (domainPacketType == PacketType::DomainConnectRequest) {
#if (PR_BUILD || DEV_BUILD)
if (_shouldSendNewerVersion) {
domainPacket->setVersion(versionForPacketType(domainPacketType) + 1);
}
#endif
QUuid connectUUID;
if (!_domainHandler.getAssignmentUUID().isNull()) {
@ -315,18 +320,14 @@ void NodeList::sendDomainServerCheckIn() {
packetStream << connectUUID;
// include the protocol version signature in our connect request
if (_domainConnectRequestVersion >= static_cast<PacketVersion>(DomainConnectRequestVersion::HasProtocolVersions)) {
QByteArray protocolVersionSig = protocolVersionsSignature();
packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size());
}
QByteArray protocolVersionSig = protocolVersionsSignature();
packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size());
}
// pack our data to send to the domain-server including
// the hostname information (so the domain-server can see which place name we came in on)
packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList();
if (_domainConnectRequestVersion >= static_cast<PacketVersion>(DomainConnectRequestVersion::HasHostname)) {
packetStream << DependencyManager::get<AddressManager>()->getPlaceName();
}
packetStream << DependencyManager::get<AddressManager>()->getPlaceName();
if (!_domainHandler.isConnected()) {
DataServerAccountInfo& accountInfo = accountManager->getAccountInfo();
@ -341,9 +342,7 @@ void NodeList::sendDomainServerCheckIn() {
flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SendDSCheckIn);
if (!isUsingDTLS) {
sendPacket(std::move(domainPacket), _domainHandler.getSockAddr());
}
sendPacket(std::move(domainPacket), _domainHandler.getSockAddr());
if (_numNoReplyDomainCheckIns >= MAX_SILENT_DOMAIN_SERVER_CHECK_INS) {
// we haven't heard back from DS in MAX_SILENT_DOMAIN_SERVER_CHECK_INS
@ -462,7 +461,7 @@ void NodeList::handleICEConnectionToDomainServer() {
LimitedNodeList::sendPeerQueryToIceServer(_domainHandler.getICEServerSockAddr(),
_domainHandler.getICEClientID(),
_domainHandler.getICEDomainID());
_domainHandler.getPendingDomainID());
}
}
@ -475,7 +474,7 @@ void NodeList::pingPunchForDomainServer() {
if (_domainHandler.getICEPeer().getConnectionAttempts() == 0) {
qCDebug(networking) << "Sending ping packets to establish connectivity with domain-server with ID"
<< uuidStringWithoutCurlyBraces(_domainHandler.getICEDomainID());
<< uuidStringWithoutCurlyBraces(_domainHandler.getPendingDomainID());
} else {
if (_domainHandler.getICEPeer().getConnectionAttempts() % NUM_DOMAIN_SERVER_PINGS_BEFORE_RESET == 0) {
// if we have then nullify the domain handler's network peer and send a fresh ICE heartbeat
@ -527,7 +526,7 @@ void NodeList::processDomainServerList(QSharedPointer<ReceivedMessage> message)
DependencyManager::get<NodeList>()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSList);
QDataStream packetStream(message->getMessage());
// grab the domain's ID from the beginning of the packet
QUuid domainUUID;
packetStream >> domainUUID;
@ -543,14 +542,11 @@ void NodeList::processDomainServerList(QSharedPointer<ReceivedMessage> message)
packetStream >> newUUID;
setSessionUUID(newUUID);
quint8 isAllowedEditor;
packetStream >> isAllowedEditor;
setIsAllowedEditor((bool) isAllowedEditor);
// pull the permissions/right/privileges for this node out of the stream
NodePermissions newPermissions;
packetStream >> newPermissions;
setPermissions(newPermissions);
quint8 thisNodeCanRez;
packetStream >> thisNodeCanRez;
setThisNodeCanRez((bool) thisNodeCanRez);
// pull each node in the packet
while (packetStream.device()->pos() < message->getSize()) {
parseNodeFromPacketStream(packetStream);
@ -577,10 +573,9 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) {
qint8 nodeType;
QUuid nodeUUID, connectionUUID;
HifiSockAddr nodePublicSocket, nodeLocalSocket;
bool isAllowedEditor;
bool canRez;
NodePermissions permissions;
packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> isAllowedEditor >> canRez;
packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> permissions;
// if the public socket address is 0 then it's reachable at the same IP
// as the domain server
@ -591,8 +586,7 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) {
packetStream >> connectionUUID;
SharedNodePointer node = addOrUpdateNode(nodeUUID, nodeType, nodePublicSocket,
nodeLocalSocket, isAllowedEditor, canRez,
connectionUUID);
nodeLocalSocket, permissions, connectionUUID);
}
void NodeList::sendAssignment(Assignment& assignment) {

View file

@ -68,9 +68,6 @@ public:
void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; }
/// downgrades the DomainConnnectRequest PacketVersion to attempt to probe for older domain servers
void downgradeDomainServerCheckInVersion() { _domainConnectRequestVersion--; }
public slots:
void reset();
void sendDomainServerCheckIn();
@ -88,8 +85,9 @@ public slots:
void processICEPingPacket(QSharedPointer<ReceivedMessage> message);
void resetDomainServerCheckInVersion()
{ _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest); }
#if (PR_BUILD || DEV_BUILD)
void toggleSendNewerDSConnectVersion(bool shouldSendNewerVersion) { _shouldSendNewerVersion = shouldSendNewerVersion; }
#endif
signals:
void limitOfSilentDomainCheckInsReached();
@ -105,6 +103,7 @@ private slots:
void pingPunchForDomainServer();
void sendKeepAlivePings();
private:
NodeList() : LimitedNodeList(0, 0) { assert(false); } // Not implemented, needed for DependencyManager templates compile
NodeList(char ownerType, unsigned short socketListenPort = 0, unsigned short dtlsListenPort = 0);
@ -130,7 +129,9 @@ private:
bool _isShuttingDown { false };
QTimer _keepAlivePingTimer;
PacketVersion _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest);
#if (PR_BUILD || DEV_BUILD)
bool _shouldSendNewerVersion { false };
#endif
};
#endif // hifi_NodeList_h

View file

@ -0,0 +1,97 @@
//
// NodePermissions.cpp
// libraries/networking/src/
//
// Created by Seth Alves on 2016-6-1.
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QDataStream>
#include <QtCore/QDebug>
#include "NodePermissions.h"
QString NodePermissions::standardNameLocalhost = QString("localhost");
QString NodePermissions::standardNameLoggedIn = QString("logged-in");
QString NodePermissions::standardNameAnonymous = QString("anonymous");
QStringList NodePermissions::standardNames = QList<QString>()
<< NodePermissions::standardNameLocalhost
<< NodePermissions::standardNameLoggedIn
<< NodePermissions::standardNameAnonymous;
NodePermissions& NodePermissions::operator|=(const NodePermissions& rhs) {
this->canConnectToDomain |= rhs.canConnectToDomain;
this->canAdjustLocks |= rhs.canAdjustLocks;
this->canRezPermanentEntities |= rhs.canRezPermanentEntities;
this->canRezTemporaryEntities |= rhs.canRezTemporaryEntities;
this->canWriteToAssetServer |= rhs.canWriteToAssetServer;
this->canConnectPastMaxCapacity |= rhs.canConnectPastMaxCapacity;
return *this;
}
NodePermissions& NodePermissions::operator|=(const NodePermissionsPointer& rhs) {
if (rhs) {
*this |= *rhs.get();
}
return *this;
}
NodePermissionsPointer& operator|=(NodePermissionsPointer& lhs, const NodePermissionsPointer& rhs) {
if (lhs && rhs) {
*lhs.get() |= rhs;
}
return lhs;
}
QDataStream& operator<<(QDataStream& out, const NodePermissions& perms) {
out << perms.canConnectToDomain;
out << perms.canAdjustLocks;
out << perms.canRezPermanentEntities;
out << perms.canRezTemporaryEntities;
out << perms.canWriteToAssetServer;
out << perms.canConnectPastMaxCapacity;
return out;
}
QDataStream& operator>>(QDataStream& in, NodePermissions& perms) {
in >> perms.canConnectToDomain;
in >> perms.canAdjustLocks;
in >> perms.canRezPermanentEntities;
in >> perms.canRezTemporaryEntities;
in >> perms.canWriteToAssetServer;
in >> perms.canConnectPastMaxCapacity;
return in;
}
QDebug operator<<(QDebug debug, const NodePermissions& perms) {
debug.nospace() << "[permissions: " << perms.getID() << " --";
if (perms.canConnectToDomain) {
debug << " connect";
}
if (perms.canAdjustLocks) {
debug << " locks";
}
if (perms.canRezPermanentEntities) {
debug << " rez";
}
if (perms.canRezTemporaryEntities) {
debug << " rez-tmp";
}
if (perms.canWriteToAssetServer) {
debug << " asset-server";
}
if (perms.canConnectPastMaxCapacity) {
debug << " ignore-max-cap";
}
debug.nospace() << "]";
return debug.nospace();
}
QDebug operator<<(QDebug debug, const NodePermissionsPointer& perms) {
if (perms) {
return operator<<(debug, *perms.get());
}
debug.nospace() << "[permissions: null]";
return debug.nospace();
}

View file

@ -0,0 +1,97 @@
//
// NodePermissions.h
// libraries/networking/src/
//
// Created by Seth Alves on 2016-6-1.
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_NodePermissions_h
#define hifi_NodePermissions_h
#include <memory>
#include <QString>
#include <QMap>
#include <QVariant>
#include <QUuid>
class NodePermissions;
using NodePermissionsPointer = std::shared_ptr<NodePermissions>;
class NodePermissions {
public:
NodePermissions() { _id = QUuid::createUuid().toString(); }
NodePermissions(const QString& name) { _id = name; }
NodePermissions(QMap<QString, QVariant> perms) {
_id = perms["permissions_id"].toString();
canConnectToDomain = perms["id_can_connect"].toBool();
canAdjustLocks = perms["id_can_adjust_locks"].toBool();
canRezPermanentEntities = perms["id_can_rez"].toBool();
canRezTemporaryEntities = perms["id_can_rez_tmp"].toBool();
canWriteToAssetServer = perms["id_can_write_to_asset_server"].toBool();
canConnectPastMaxCapacity = perms["id_can_connect_past_max_capacity"].toBool();
}
QString getID() const { return _id; }
// the _id member isn't authenticated and _username is.
void setUserName(QString userName) { _userName = userName; }
QString getUserName() { return _userName; }
bool isAssignment { false };
// these 3 names have special meaning.
static QString standardNameLocalhost;
static QString standardNameLoggedIn;
static QString standardNameAnonymous;
static QStringList standardNames;
// the initializations here should match the defaults in describe-settings.json
bool canConnectToDomain { true };
bool canAdjustLocks { false };
bool canRezPermanentEntities { false };
bool canRezTemporaryEntities { false };
bool canWriteToAssetServer { false };
bool canConnectPastMaxCapacity { false };
void setAll(bool value) {
canConnectToDomain = value;
canAdjustLocks = value;
canRezPermanentEntities = value;
canRezTemporaryEntities = value;
canWriteToAssetServer = value;
canConnectPastMaxCapacity = value;
}
QVariant toVariant() {
QMap<QString, QVariant> values;
values["permissions_id"] = _id;
values["id_can_connect"] = canConnectToDomain;
values["id_can_adjust_locks"] = canAdjustLocks;
values["id_can_rez"] = canRezPermanentEntities;
values["id_can_rez_tmp"] = canRezTemporaryEntities;
values["id_can_write_to_asset_server"] = canWriteToAssetServer;
values["id_can_connect_past_max_capacity"] = canConnectPastMaxCapacity;
return QVariant(values);
}
NodePermissions& operator|=(const NodePermissions& rhs);
NodePermissions& operator|=(const NodePermissionsPointer& rhs);
friend QDataStream& operator<<(QDataStream& out, const NodePermissions& perms);
friend QDataStream& operator>>(QDataStream& in, NodePermissions& perms);
protected:
QString _id;
QString _userName;
};
const NodePermissions DEFAULT_AGENT_PERMISSIONS;
QDebug operator<<(QDebug debug, const NodePermissions& perms);
QDebug operator<<(QDebug debug, const NodePermissionsPointer& perms);
NodePermissionsPointer& operator|=(NodePermissionsPointer& lhs, const NodePermissionsPointer& rhs);
#endif // hifi_NodePermissions_h

View file

@ -45,7 +45,7 @@ const QSet<PacketType> RELIABLE_PACKETS = QSet<PacketType>();
PacketVersion versionForPacketType(PacketType packetType) {
switch (packetType) {
case PacketType::DomainList:
return 18;
return static_cast<PacketVersion>(DomainListVersion::PermissionsGrid);
case PacketType::EntityAdd:
case PacketType::EntityEdit:
case PacketType::EntityData:
@ -69,6 +69,9 @@ PacketVersion versionForPacketType(PacketType packetType) {
case PacketType::DomainConnectRequest:
return static_cast<PacketVersion>(DomainConnectRequestVersion::HasProtocolVersions);
case PacketType::DomainServerAddedNode:
return static_cast<PacketVersion>(DomainServerAddedNodeVersion::PermissionsGrid);
default:
return 17;
}

View file

@ -199,4 +199,14 @@ enum class DomainConnectionDeniedVersion : PacketVersion {
IncludesReasonCode
};
enum class DomainServerAddedNodeVersion : PacketVersion {
PrePermissionsGrid = 17,
PermissionsGrid
};
enum class DomainListVersion : PacketVersion {
PrePermissionsGrid = 18,
PermissionsGrid
};
#endif // hifi_PacketHeaders_h

View file

@ -1863,9 +1863,9 @@ bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputSt
QJsonDocument asDocument = QJsonDocument::fromJson(jsonBuffer);
QVariant asVariant = asDocument.toVariant();
QVariantMap asMap = asVariant.toMap();
readFromMap(asMap);
bool success = readFromMap(asMap);
delete[] rawData;
return true;
return success;
}
void Octree::writeToFile(const char* fileName, OctreeElementPointer element, QString persistAsFileType) {

View file

@ -213,10 +213,12 @@ QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath, bool
if (shouldCreateIfMissing || variantMap.contains(firstKey)) {
if (dotIndex == -1) {
return &variantMap[firstKey];
} else if (variantMap[firstKey].canConvert(QMetaType::QVariantMap)) {
return valueForKeyPath(*static_cast<QVariantMap*>(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1),
shouldCreateIfMissing);
}
if (!variantMap[firstKey].canConvert(QMetaType::QVariantMap)) {
variantMap[firstKey] = QVariantMap();
}
return valueForKeyPath(*static_cast<QVariantMap*>(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1),
shouldCreateIfMissing);
}
return NULL;

View file

@ -1222,7 +1222,7 @@ function handeMenuEvent(menuItem) {
Window.alert("No entities have been selected.");
} else {
var filename = "entities__" + Window.location.hostname + ".svo.json";
filename = Window.save("Select where to save", filename, "*.json")
filename = Window.save("Select Where to Save", filename, "*.json")
if (filename) {
var success = Clipboard.exportEntities(filename, selectionManager.selections);
if (!success) {
@ -1234,7 +1234,7 @@ function handeMenuEvent(menuItem) {
var importURL = null;
if (menuItem == "Import Entities") {
var fullPath = Window.browse("Select models to import", "", "*.json");
var fullPath = Window.browse("Select Model to Import", "", "*.json");
if (fullPath) {
importURL = "file:///" + fullPath;
}