From aee24fb41c8b1787c4b3bcb787a1effa6feed2a8 Mon Sep 17 00:00:00 2001 From: Anna Puddles <113144806+annapuddles@users.noreply.github.com> Date: Tue, 24 Jan 2023 13:14:59 -0500 Subject: [PATCH] Add file server request handler Add a file server request handler that serves notecards from the prim's inventory. --- file server/README.md | 14 ++ file server/index.xhtml | 12 + file server/prim-dns file server cache.lsl | 111 ++++++++++ file server/prim-dns file server.lsl | 241 +++++++++++++++++++++ prim-dns.lsl | 25 ++- request handler.lsl | 10 +- 6 files changed, 401 insertions(+), 12 deletions(-) create mode 100644 file server/README.md create mode 100644 file server/index.xhtml create mode 100644 file server/prim-dns file server cache.lsl create mode 100644 file server/prim-dns file server.lsl diff --git a/file server/README.md b/file server/README.md new file mode 100644 index 0000000..3aa5bf7 --- /dev/null +++ b/file server/README.md @@ -0,0 +1,14 @@ +# prim-dns file server + +The prim-dns file server is a request handler script which will serve specially-named notecards in the prim's inventory as "files". Notecards must be named after the path of the file, including slashes. The example [index.xhtml](index.xhtml) notecard would actually be named `/index.xhtml` in the prim's inventory. + +The script will determine the MIME type from the "extension" in the name of the notecard. For example, `/index.xhtml` will use `CONTENT_TYPE_XHTML` (`application/xhtml+xml`), while `/data.json` will use `CONTENT_TYPE_JSON` (`application/json`). [The available MIME types are limited](https://wiki.secondlife.com/wiki/LlSetContentType) and cannot be customized. Anything without a known extension will be treated as `CONTENT_TYPE_TEXT` (`text/plain`). + +Paths that end in `/` are treated as "directories". The script will attempt to serve a corresponding `index.xhtml` notecard for the "directory". For example, if the path is `/example/`, the script will attempt to server the contents of a notecard named `/example/index.xhtml`. + +Paths with no file extension that do not match a notecard with the appropriate name are also treated as "directories", but will redirect to add the trailing `/`. For example, if the path is `/example`, and there is no notecard named `/example` in the inventory, then the page will redirect to `/example/`. + +When using the file server, all paths that do not match a notecard will return a 404 error. In order to combine the file server with other request handler scripts that dynamically handle some paths, those scripts must register the paths they will handle with the file server. This is done by sending the `prim-dns:file-server:register-path` notification to the file server script via a JSON-RPC link message: +```lsl +jsonrpc_link_notification(LINK_SET, "prim-dns:file-server:register-path", JSON_OBJECT, ["path", "/data.json"]); +``` diff --git a/file server/index.xhtml b/file server/index.xhtml new file mode 100644 index 0000000..e30bbff --- /dev/null +++ b/file server/index.xhtml @@ -0,0 +1,12 @@ + + + +
+If you can read this, the file server is working!
+ + diff --git a/file server/prim-dns file server cache.lsl b/file server/prim-dns file server cache.lsl new file mode 100644 index 0000000..dd8f096 --- /dev/null +++ b/file server/prim-dns file server cache.lsl @@ -0,0 +1,111 @@ +/* The content of the current notecard being read. */ +string notecard_content; + +/* The current line of the current notecard being read. */ +integer notecard_line; + +/* The index of the current notecard being read. */ +integer notecard_index; + +/* The name of the current notecard being read. */ +string notecard_name; + +/* The index of the last notecard that this cache script should read. */ +integer max_notecard_index; + +key notecard_query; + +/* A strided list of notecard names and their contents. */ +list cache; + +string jsonrpc_notification(string method, string params_type, list params) +{ + return llList2Json(JSON_OBJECT, ["jsonrpc", "2.0", "method", method, "params", llList2Json(params_type, params)]); +} + +jsonrpc_link_notification(integer link, string method, string params_type, list params) +{ + llMessageLinked(link, 0, jsonrpc_notification(method, params_type, params), NULL_KEY); +} + +/* Read the next notecard into the cache. */ +read_next_notecard() +{ + notecard_content = ""; + + if (notecard_index <= max_notecard_index) + { + notecard_name = llGetInventoryName(INVENTORY_NOTECARD, notecard_index++); + notecard_line = 0; + notecard_query = llGetNotecardLine(notecard_name, notecard_line++); + } + else + { + notecard_name = ""; + llOwnerSay(llGetScriptName() + ": " + (string) (llGetUsedMemory() / 1024) + " KiB"); + } +} + +/* Send a cached notecard's contents back to the main script if this script has it. */ +send_cached_notecard(integer sender, key request_id, string name) +{ + integer index = llListFindList(cache, [name]); + + if (index == -1) + { + return; + } + + jsonrpc_link_notification(sender, "prim-dns:file-server:cache:response", JSON_OBJECT, ["request-id", request_id, "body", llList2String(cache, index + 1)]); +} + +default +{ + link_message(integer sender, integer num, string str, key id) + { + string method = llJsonGetValue(str, ["method"]); + + if (method == "prim-dns:file-server:cache:read") + { + string script = llJsonGetValue(str, ["params", "script"]); + + if (script != llGetScriptName()) + { + return; + } + + cache = []; + + notecard_index = (integer) llJsonGetValue(str, ["params", "start"]); + max_notecard_index = (integer) llJsonGetValue(str, ["params", "end"]); + + read_next_notecard(); + } + else if (method == "prim-dns:file-server:cache:send") + { + key request_id = (key) llJsonGetValue(str, ["params", "request-id"]); + string name = llJsonGetValue(str, ["params", "name"]); + + send_cached_notecard(sender, request_id, name); + } + } + + dataserver(key id, string data) + { + if (id != notecard_query) + { + return; + } + + if (data == EOF) + { + cache += [notecard_name, notecard_content]; + read_next_notecard(); + } + else + { + notecard_content += data + "\n"; + notecard_query = llGetNotecardLine(notecard_name, notecard_line++); + } + } +} diff --git a/file server/prim-dns file server.lsl b/file server/prim-dns file server.lsl new file mode 100644 index 0000000..d57c12c --- /dev/null +++ b/file server/prim-dns file server.lsl @@ -0,0 +1,241 @@ +/* The prefix for scripts which will act as caches for the file server. */ +string cache_prefix = "prim-dns file server cache"; + +/* The name used for the index notecard in each "directory". Note: XHTML is preferred because HTML can only be used by the internal SL browser, and only works for the owner of the linkset. */ +string index_notecard = "index.xhtml"; + +/* A list of cached notecards, the contents of which are stored in the cache scripts. */ +list cached_notecards; + +/* Paths registered by other request handler scripts that should not be treated as files. */ +list registered_paths; + +/* JSON-RPC functions */ +string jsonrpc_request(string method, string params_type, list params, string id) +{ + if (id == "") id = (string) llGenerateKey(); + return llList2Json(JSON_OBJECT, ["jsonrpc", "2.0", "id", id, "method", method, "params", llList2Json(params_type, params)]); +} + +string jsonrpc_notification(string method, string params_type, list params) +{ + return llList2Json(JSON_OBJECT, ["jsonrpc", "2.0", "method", method, "params", llList2Json(params_type, params)]); +} + +string jsonrpc_link_request(integer link, string method, string params_type, list params, string id) +{ + if (id == "") id = (string) llGenerateKey(); + llMessageLinked(link, 0, jsonrpc_request(method, params_type, params, id), NULL_KEY); + return id; +} + +jsonrpc_link_notification(integer link, string method, string params_type, list params) +{ + llMessageLinked(link, 0, jsonrpc_notification(method, params_type, params), NULL_KEY); +} + +/* Begin reading all the notecards into the cache */ +store_notecards_in_cache() +{ + llOwnerSay("Updating notecard cache..."); + + /* Clear the existing list of cached notecards */ + cached_notecards = []; + + /* The prim will have one or more cache scripts that start with the cache_prefix. + * The contents of the cached notecards will be stored evenly among these scripts, + * allowing the cache to be as big as desired by simply adding more cache scripts. + */ + + /* First, we get a list of all the cache scripts in the prim. */ + list cache_scripts; + integer scripts = llGetInventoryNumber(INVENTORY_SCRIPT); + integer i; + + for (i = 0; i <= scripts; ++i) + { + string name = llGetInventoryName(INVENTORY_SCRIPT, i); + + if (llGetSubString(name, 0, llStringLength(cache_prefix) - 1) == cache_prefix) + { + cache_scripts += name; + } + } + + integer total_cache_scripts = llGetListLength(cache_scripts); + + /* If there are no cache scripts, abort. */ + if (total_cache_scripts == 0) + { + return; + } + + /* Next, get a list of all the notecards we need to cache. */ + integer notecards = llGetInventoryNumber(INVENTORY_NOTECARD); + + for (i = 0; i < notecards; ++i) + { + string name = llGetInventoryName(INVENTORY_NOTECARD, i); + + /* Only cache notecards starting with / */ + if (llGetSubString(name, 0, 0) == "/") + { + cached_notecards += llGetInventoryName(INVENTORY_NOTECARD, i); + } + } + + integer total_cached_notecards = llGetListLength(cached_notecards); + + /* Calculate how many notecards will be cached per script. */ + integer notecards_per_script = llCeil(total_cached_notecards / total_cache_scripts); + + /* Assign cache scripts to ranges of notecards based on the above. */ + for (i = 0; i < total_cache_scripts; ++i) + { + integer min = notecards_per_script * i; + integer max = notecards_per_script * (i + 1) - 1; + + jsonrpc_link_notification(LINK_SET, "prim-dns:file-server:cache:read", JSON_OBJECT, ["script", llList2String(cache_scripts, i), "start", min, "end", max]); + } +} + +/* Perform a redirect using XHTML + Javascript, since we can't set the Location header of a response */ +redirect(integer sender, key request_id, string location) +{ + jsonrpc_link_notification(sender, "prim-dns:set-content-type", JSON_OBJECT, ["request-id", request_id, "content-type", CONTENT_TYPE_XHTML]); + jsonrpc_link_notification(sender, "prim-dns:response", JSON_OBJECT, ["request-id", request_id, "status", 200, "body", "Hello, world!"]), NULL_KEY); + jsonrpc_link_notification(sender, "prim-dns:set-content-type", JSON_OBJECT, ["request-id", request_id, "content-type", CONTENT_TYPE_XHTML]); + jsonrpc_link_notification(sender, "prim-dns:response", JSON_OBJECT, ["request-id", request_id, "status", 200, "body", "Hello, world!"]); } } }