As it stands, two scopes are provided: owner and object.
In "owner" scope, multiple objects with the same owner share a keyspace, regardless of where they are in-world and without prior knowledge of their UUIDs.
In "object" scope, each object gets a private keyspace dependent on its UUID. (I.e. subject to reset by all the same factors that change an objects UUID.)
Additional scopes may be added (using strong authentication to control access) that will enable other types of inter-object communication that are not ownership-dependent.
Some documentation is provided in the form of comments.
A small example usage is provided at the bottom. Rez two cubes with this script, move them at least 20 meters apart, then say something in front of one and click the other to see the last overheard message.
A free server implementation is provided for you, as the server-side code uses non-portable functionality in order to achieve high performance. Just use the specified service URL. Portable source code may be released at a future time.
CODE
//
// SLAssoc v1.0
//
// This script was developed by Tuach Noh as an example of the interface to the
// free SL persistent associative array storage offered at sl.nfshost.com.
//
// Permission is hereby given to all persons to use this script and/or the
// associated server facility for any lawful purpose within Second Life.
//
// Do not IM Tuach Noh in-world for technical support related to this script or
// aliens will eat your brain!
//
//
// Theory of Operation
//
// This script provides a series of functions designed to easily store and retrieve
// persistent data from a free server set up for that purpose. To set data, it
// sends an HTTP PUT request (Never seen a PUT request before? You have now.) and
// to retrieve it, it sends an HTTP GET request.
//
// The key being put/requested is part of the URL; valid key values are
// alphanumeric, and keys are case insensitive. The value associated with a
// key is stored in the HTTP request (or response) body. This avoids a lot of
// goofy problems with trying to encode data into a URL, but it means that
// the practical length limit for data values is about 2k (less if you use
// the XOR option) due to LSL limitations on the HTTP body size.
//
// If a requested key is found, HTTP status 200 is returned. If the key is not
// found, HTTP status 404 is returned. A variety of other HTTP status codes may
// appear for other types of problems; the server will attempt to populate the
// response body with useful information if this is the case.
//
// Presently there are two scopes: owner and object. If you use the owner scope
// (slRequestOwnerKey and slPutOwnerKey), all objects with a given owner will share
// keyspace. If you use the object scope (slRequestObjectKey and slPutObjectKey),
// each object will have a private keyspace.
//
// It is not possible to query the web server for keys in the owner and object
// scopes from outside of Second Life.
//
//
// LSL Improvements that would make this better:
//
// 1) Close your eyes and wish hard for llSyncHttpRequest() that runs synchronously
// and has as its return value a list containing the parameters of the current
// http_response() event. That would make it possible to have an interface similar
// to the existing llList2...() functions, a vastly more powerful and intuitive
// interface than being forced to wait for response events.
//
// 2) It would be nice to see support for the HTTP DELETE method. This would be
// trivial to do and would
//
// It would be great to have twofish (or another strong block cipher) support in LSL
// to provide better protection of transmitted data. (See XOR discussion below.)
//
//
// Notes about XOR scrambling:
//
// The first question that should come to your mind is "If I use this server to
// store sensitive object data, how can I be sure the people on the server won't
// read my data?"
//
// The solution is to use the provided XOR scrambling. (It's not quite strong
// enough to merit being called "encryption.") Choosing a long, strong XOR
// secret will render your data values mostly opaque.
//
// However, do not overestimate the security of the provided XOR
// implementation; it's not that great. With enough ciphertext, one could
// theoretically use statistical analysis to recover the plaintext. Lobby
// for llTwofish2String() and llString2Twofish() if you don't like that.
//
// Keys are not scrambled due to the unfortunate presence of / in the Base64
// character set.
//
// Print copious debugging information about requests and lists.
integer g_bDebug = FALSE;
// If this is set, non-error responses to PUT requests will be ignored.
// This is useful because you usually don't care that yes, in fact, it worked.
integer g_bIgnorePutResponses = TRUE;
// Use this to scramble your data so that it can't be read on the server side.
string g_strXORSecret = "I did not read the instructions and don't deserve any security.";
// string g_strXORSecret = ""; // No XOR scrambling desired.
//
// These are the internal variables used. You probably shouldn't change them.
//
string g_strServiceURL = "http://sl.nfshost.com/assoc/";
integer g_iRequestListStride = 4;
list g_lRequests = [];
// If you are a genius and you want better protection, set g_strXORSecret
// to "yes" and set this to a legal base64 encoded string at least 256
// characters in length (preferably one that would be purely random data when
// decoded).
string g_strXORSecret64 = "";
//
// These are internal functions. You should probably not call them directly.
//
string _slDecode(string i_str) {
if (g_strXORSecret == "") return i_str;
if (g_strXORSecret64 == "")
g_strXORSecret64 = llStringToBase64(g_strXORSecret);
return llBase64ToString(llXorBase64StringsCorrect(i_str, g_strXORSecret64));
}
string _slEncode(string i_str) {
if (g_strXORSecret == "") return i_str;
if (g_strXORSecret64 == "")
g_strXORSecret64 = llStringToBase64(g_strXORSecret);
return llXorBase64StringsCorrect(llStringToBase64(i_str), g_strXORSecret64);
}
list _slExtractRequest(key i_kRequest) {
integer i;
list l;
for (i=0;i<llGetListLength(g_lRequests);i += g_iRequestListStride) {
if (llList2Key(g_lRequests,i) == i_kRequest) {
l = llList2List(g_lRequests,i,i + g_iRequestListStride - 1);
g_lRequests = llDeleteSubList(g_lRequests, i, i + g_iRequestListStride - 1);
if (g_bDebug) {
_slDebugList(l);
_slDebugList(g_lRequests);
}
return l;
}
}
return [];
}
integer _slPutKey(string i_strScope, string i_strKey, string i_strValue) {
key kRequest;
kRequest = llHTTPRequest(g_strServiceURL + i_strScope + "/" + i_strKey, [ HTTP_METHOD, "PUT"], _slEncode(i_strValue));
if (kRequest == NULL_KEY) return FALSE;
_slSaveRequest(kRequest,"PUT",i_strScope,i_strKey);
return TRUE;
}
integer _slRequestKey(string i_strScope, string i_strKey) {
key kRequest;
kRequest = llHTTPRequest(g_strServiceURL + i_strScope + "/" + i_strKey, [], "");
if (kRequest == NULL_KEY) return FALSE;
_slSaveRequest(kRequest,"GET",i_strScope,i_strKey);
return TRUE;
}
_slSaveRequest(key i_kRequest, string i_strMethod, string i_strScope, string i_strKey) {
g_lRequests = (g_lRequests=[]) + g_lRequests + [ i_kRequest, i_strMethod, i_strScope, i_strKey ];
}
_slDebugHTTPResponse(integer i_iStatus, string i_strBody) {
list lBody;
integer i;
llOwnerSay("HTTP Response Status: " + (string)i_iStatus);
llOwnerSay("Body Follows:");
lBody = llParseStringKeepNulls(i_strBody,[ "\n" ], [ "" ]);
for (i=0;i<llGetListLength(lBody);i++)
llOwnerSay(llList2String(lBody,i));
}
_slDebugList(list i_l) {
integer i;
integer iMax = llGetListLength(i_l);
llOwnerSay("List has " + (string)iMax + " elements.");
for(i=0;i<iMax;i++)
llOwnerSay((string)i + ". " + llList2String(i_l,i));
}
//
// These are public functions.
//
integer slRequestObjectKey(string i_strKey) {
return _slRequestKey("object",i_strKey);
}
integer slRequestOwnerKey(string i_strKey) {
return _slRequestKey("owner",i_strKey);
}
string slResponseBody(list l) {
return llList2String(l,6);
}
integer slResponseIsSuccess(list l) {
return llList2Integer(l,4) == TRUE;
}
integer slPutObjectKey(string i_strKey, string i_strValue) {
return _slPutKey("object",i_strKey,i_strValue);
}
integer slPutOwnerKey(string i_strKey, string i_strValue) {
return _slPutKey("owner",i_strKey,i_strValue);
}
list slHandleHTTPResponse(key i_kRequest, integer i_iStatus, list i_lMeta, string i_strBody) {
list l;
integer bOk = FALSE;
if (g_bDebug) _slDebugHTTPResponse(i_iStatus,i_strBody);
l = _slExtractRequest(i_kRequest);
if (l == []) return [];
if (i_iStatus == 200) {
bOk = TRUE;
i_strBody = _slDecode(i_strBody);
}
if ((llList2String(l,1) == "PUT") && bOk && g_bIgnorePutResponses) return [];
return l + bOk + i_iStatus + i_strBody;
}
//
// End of the persistent assoc stuff.
//
//
// This is a silly little demo that parrots back whatever the last thing said when
// you click on it. It uses the owner scope, so you can rez two cubes with this
// script and they'll share their idea of "last words" no matter where they are.
//
// If you set the g_bIgnorePutResponses flag to FALSE it will echo what it hears
// as it hears it. (Don't put two of these near each other or they will argue.)
// If you set the flag to TRUE (the default), it will wait to be touched before echoing.
//
default {
state_entry() {
llListen(0,"",NULL_KEY,"");
}
http_response(key i_kRequest, integer i_iStatus, list i_lMeta, string i_strBody) {
list l;
l = slHandleHTTPResponse(i_kRequest,i_iStatus,i_lMeta,i_strBody);
if (l == []) return;
if (!slResponseIsSuccess(l)) {
llOwnerSay("Disaster! (" + (string)i_iStatus + ")");
return;
}
llSay(0,"Famous last words: " + slResponseBody(l));
}
listen(integer i_iChannel, string i_strWho, key i_kWho, string i_strMessage) {
slPutOwnerKey("lastword",i_strMessage);
}
touch_start(integer total_number) {
slRequestOwnerKey("lastword");
}
}