Initial push

This commit is contained in:
lickx
2025-08-03 14:08:47 +02:00
parent 78085be014
commit eb9e528a0e
6 changed files with 278 additions and 2 deletions

View File

@@ -1,2 +1,36 @@
# empty-trash
Clickable terminal for emptying trash in OpenSim
# Clickable terminal for emptying trash in OpenSim
This is to secure your grid users' inventories on the hypergrid.
Especially important when your grid uses hypergrid 1.0!
In robust.ini set AllowDelete to false:
```
[InventoryService]
LocalServiceModule = "OpenSim.Services.InventoryService.dll:XInventoryService"
; Will calls to purge folders (empty trash) and immediately delete/update items or folders (not move to trash first) succeed?
; If this is set to false then some other arrangement must be made to perform these operations if necessary.
AllowDelete = false
```
If the php script only listens on your private subnet, take note that
by default, llHttpRequest (which the terminal uses) can't request webpages
from private subnets. There are two ways to address this:
1. Insecure: Remove your private subnet from the blacklist in OpenSim.ini:
```
[Network]
; 192.168.0.0/16 removed:
OutboundDisallowForUserScripts = 0.0.0.0/8|10.0.0.0/8|100.64.0.0/10|127.0.0.0/8|169.254.0.0/16|172.16.0.0/12|192.0.0.0/24|192.0.2.0/24|192.88.99.0/24|198.18.0.0/15|198.51.100.0/24|203.0.113.0/24|224.0.0.0/4|240.0.0.0/4|255.255.255.255/32
```
Caution, this would allow any lsl script (even in a visitor's attachment) to
access any webserver in that private subnet, for example your NAS storage!
2. Secure: Use [opensim-lickx](https://github.com/lickx/opensim-lickx) instead of stock OpenSim.
This (only) allows requests from the /lslhttp folder even on blacklisted private networks.
See commit 7503d1a23fd4a45742295cdb44223d2f7ac4033a if you would like to apply this
to your own OpenSim fork.
If the php script listens on the public ip (not recommended), ALLOWED_HOST should
be filled in config.php, and the relevant section uncommented in trash.php.

42
intranet-lsl Normal file
View File

@@ -0,0 +1,42 @@
# Example for making a webserver on the private subnet with nginx on port 80
server {
listen 80;
# the lsl script will do a llHttpRequest to 192.168.0.10/lslhttp/trash.php
server_name 192.168.0.10;
access_log /var/log/nginx/intranet-lsl_access.log;
error_log /var/log/nginx/intranet-lsl_error.log;
# within this folder, you'd make the lslhttp folder:
root /var/www/intranet-lsl;
# Add index.php to the list if you are using PHP
index index.html index.htm index.php;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
# pass PHP scripts to FastCGI server
#
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# With php-fpm (or other unix sockets):
fastcgi_pass unix:/run/php/php-fpm.sock;
# With php-cgi (or other tcp sockets):
#fastcgi_pass 127.0.0.1:9000;
}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
location ~ /\.ht {
deny all;
}
}

43
inworld/terminal.lsl Normal file
View File

@@ -0,0 +1,43 @@
key g_kHttpRequest;
key g_kUser = NULL_KEY;
default
{
state_entry()
{
}
touch_end(integer i)
{
key kID = llDetectedKey(0);
if (kID == NULL_KEY) return;
if (g_kUser != NULL_KEY) return; // in use by another user
string sName = llKey2Name(kID);
if (~llSubStringIndex(sName, "@")) {
llDialog(kID, "This terminal can only be used by local grid residents", ["OK"], -1234);
return;
}
g_kUser = kID;
llSetText("In use by "+sName, <1,1,1>, 1);
string sBody = "userID="+(string)g_kUser;
g_kHttpRequest = llHTTPRequest(
"http://192.168.0.10/lslhttp/trash.php",
[HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/x-www-form-urlencoded"],
sBody);
}
http_response(key kHttpRequest, integer iStatus, list lMetadata, string sBody)
{
if (kHttpRequest != g_kHttpRequest) return;
if (iStatus == 200) {
llDialog(g_kUser, sBody, ["OK"], -1234);
} else {
llDialog(g_kUser, "Status: "+(string)iStatus, ["OK"], -1234);
}
g_kUser = NULL_KEY;
llSetText("", <1,1,1>, 1);
}
}

9
lslhttp/index.html Normal file
View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>lsl</title>
</head>
<body>
<p></p>
</body>
</html>

135
lslhttp/trash.php Normal file
View File

@@ -0,0 +1,135 @@
<?php
include("trash_config.php");
// Don't change below this line -----------------------------------------
$NULL_KEY = "00000000-0000-0000-0000-000000000000";
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
http_response_code(404);
die();
}
// Only inworld objects owned by ALLOWED_STAFF may call this script
$objectOwner = isset($_SERVER["HTTP_X_SECONDLIFE_OWNER_KEY"]) ? $_SERVER["HTTP_X_SECONDLIFE_OWNER_KEY"] : $NULL_KEY;
if ($objectOwner != $ALLOWED_STAFF || $objectOwner == $NULL_KEY || strlen($objectOwner) != 36) {
http_response_code(401);
die();
}
// COMMENT OUT WHEN ONLY LISTENING ON A PRIVATE IP, SUCH AS 192.168.0.10
// NEEDED WHEN LISTENING ON A PUBLIC IP! FILL IN ALLOWED_HOST ABOVE WITH THE
// FQDN OF THE SERVER THAT RUNS THE SIM WITH THE TRASH TERMINAL.
/*
// Only grid-local inworld objects hosted on ALLOWED_HOST may call this script
$hostip = isset($_SERVER["REMOTE_ADDR"]) ? $_SERVER["REMOTE_ADDR"] : "unknown";
if ($hostip == "unknown") {
// couldn't resolve client ip
http_response_code(401);
die();
} else {
$hostname = gethostbyaddr($hostip);
if (!str_ends_with($hostname, $ALLOWED_HOST)) {
// end of hostname DOESN'T match $ALLOWED_HOST
http_response_code(401);
die();
}
}
*/
// Get the user of whom to empty trash for
$userID = isset($_POST["userID"]) ? $_POST["userID"] : $NULL_KEY;
if ($userID == $NULL_KEY || strlen($userID) != 36) {
http_response_code(403);
die();
}
function GetSubFolders($parentFolder)
{
global $conn, $userID;
$sql = "SELECT folderName, folderID FROM inventoryfolders WHERE parentFolderID=? AND agentID=?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $parentFolder, $userID);
$stmt->execute();
$result = $stmt->get_result();
$folderList = array();
if ($result->num_rows > 0) {
for ($i = 0; $i < $result->num_rows; $i++) {
$row = $result->fetch_assoc();
$folderName = $row["folderName"];
$folderID = $row["folderID"];
//print("Adding folder $folderName\n");
array_push($folderList, $folderID);
$subFolderIDs = GetSubFolders($folderID);
if (!empty($subFolderIDs)) {
$folderList = array_merge($folderList, $subFolderIDs);
}
}
}
$stmt->close();
return $folderList;
}
function DeleteSubFolders($parentFolder)
{
global $conn, $userID;
$sql = "DELETE FROM inventoryfolders WHERE parentFolderID=? AND agentID=?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $parentFolder, $userID);
$stmt->execute();
$stmt->close();
}
function DeleteItems($parentFolder)
{
global $conn, $userID;
$sql = "DELETE FROM inventoryitems WHERE parentFolderID=? AND avatarID=?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $parentFolder, $userID);
$stmt->execute();
$stmt->close();
}
// Create and check db connection
$conn = new mysqli($dbhost, $dbuser, $dbpass, $dbname);
if ($conn->connect_error) {
die("Database connection failed: " . $conn->connect_error);
}
// Get trash folder UUID
$sql = "SELECT folderID FROM inventoryfolders WHERE type=14 AND agentID=?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $userID);
$stmt->execute();
$result = $stmt->get_result();
$stmt->close();
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
$trashFolderID = $row["folderID"];
//print("Trash folder UUID: " . $trashFolderID . "\n");
$allTrashFolders = GetSubFolders($trashFolderID);
//print("Found " . sizeof($allTrashFolders) . " total folders in trash\n");
//print("Emptying trash...\n");
// Delete items and folders not directly under Trash rootfolder
if (sizeof($allTrashFolders) > 0) {
// Delete items and folders in every subfolder of Trash
foreach($allTrashFolders as $folder) {
DeleteItems($folder);
DeleteSubFolders($folder);
}
}
// Delete items and folders directly under Trash rootfolder
DeleteItems($trashFolderID);
DeleteSubFolders($trashFolderID);
http_response_code(200);
print("\nTrash has been emptied.\n\nYou may now also empty the Trash folder in your viewer inventory.");
} else {
http_response_code(200);
print("\nERROR: No trash folder found.\n\nContact an admin!");
}
$conn->close();
?>

13
lslhttp/trash_config.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
$dbhost = "localhost";
$dbuser = "opensim";
$dbpass = "password";
$dbname = "grid";
// Who must be the object owner that we allow calls from? Fill in the avi key:
$ALLOWED_STAFF = "777fef4a-4cc9-4637-ade2-8a80476e251c";
// Which host (running the sim with the terminal) do we allow calls from?
$ALLOWED_HOST = "mygrid.example.com";
?>