LSL Protocol/Restrained Love Relay/Reference Implementation
Restrained Life viewer v1.10 Relay Protocol Specification
By Marine Kelley
UNDER CONSTRUCTION
Audience
This document is meant for people who want to create or modify in-world objects to use the features of someone else's RestrainedLife viewer, typically cages and pieces of furniture, which per definition are usually not owned by that person.
Introduction
The RestrainedLife viewer only executes commands issued through llOwnerSay () messages. Therefore, in order to issue commands to someone using the viewer who does not own the object, that person must wear an attachment that relays commands after some security checks.
Why this spec ?
Now that RestrainedLife viewer v1.10 is out, many cages and furniture creators are interested in using its features such as sit, outfit, tp etc. These objects can be found in public places, or owned by friends... but as they are usually not owned by the user, the relay has to implement some basic security in order to avoid griefing. On top of it, the user does not want to be forced to switch to another relay when going to the next piece of furniture.
This is the purpose of this specification : to lay common rules so all the relays implementing it are compatible with all the furnitures implementing it too. Without such a specification, one cage/furniture would talk to the relay specifically made to operate with it and that's all, eventually making the creator stay behind because people rather use standard objects than proprietary closed ones.
Basic principle
Here is a sample use case :
- User is wearing a Relay
- User enters a cage in a public area
- Cage sends a chat message on a known private channel (for instance "@tploc=n")
- Relay receives the message, decides to repeat the command to the user and blocks their ability to teleport from the map with an llOwnerSay ("@tploc=n");
- User is allowed to get out after some time, the cage issues a "@tploc=y" command, immediately repeated by the relay
Without the relay, the cage could never prevent the user from teleporting since it doesn't belong to them.
Requirements
Here are the informal requirements for a relay (formal requirements below).
Security
Any object sending commands over the channel the relay listens to is likely to harm the user if there is no security implemented in the former. For instance, one could rez an object that sends a "@remoutfit=force" command over the chat channel to force an avatar to get naked in front of everyone without a warning. Of course nobody wants that, so basic security is needed.
User-friendliness
Security often implies control (access lists, switch, permissions...) so the user must be given some basic control over what kind of objects are allowed to issue commands.
Versatility
Some users will prefer wearing a dedicated attachment that they can unwear any time they want, others will be required to have the relay locked on them so they cannot detach them, others will want the script only... It is important to keep those differences in mind when deciding about the permissions of the relay. However, it is the user's responsibility to choose the relay that suits their needs best.
Licensing
According to the level of complexity and support of the relay, the creator is allowed to either provide it for free (open/close source) or to sell it, as long as it implements all the formal requirements.
Common questions
How hard is it to implement such a specification ?
That depends on what you do. Furniture/cage makers will find it very easy for it only comes down to send command over a chat channel and getting feedback. Relay makers will find it harder but then again, that depends on the level of security and user-friendliness they wish to offer. But make no mistake, the relay is what does almost all the job (along with the viewer), because there will be many more furnitures than relays around.
Why do other people need to write such a relay ? Couldn't you write it yourself and publish it ?
Of course I could, and there is even a working code for a basic relay at the end of this page. However :
- The protocol is likely to improve because nobody sees the future
- One object only would not suit everyone's needs
- It would have to implement perfect security and perfect user-friendliness, in all cases
- It would of course have to be perfectly scripted, without any bug whatsoever
The security and user-friendliness are the key parts here. Some users will prefer to be safe from griefing, others will prefer a good user interface, others will like a lot of features, others will want to move the script elsewhere... Everyone has their own tastes so there can be no one-size-fits-all relay.
Protocol
In-world objects send chat message over a channel (common to every relay and furniture), that the relay(s) acknowledges or not. If the message is a correct command and passes whatever security checks the relay implements, the latter repeats it as an llOwnerSay ().
Formal Requirements
Here
Sample code of a basic working relay
This particular example that anyone can distribute freely as open-source only (you are not allowed to sell this code) and including the header comments is just meant to give an idea of how a relay basically works.
<lsl>
//~ RestrainedLife Viewer Relay Script example code //~ By Marine Kelley //~ 2008-01-29 //~ v1.00 //~ This code is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy, //~ completeness or performance. It may not be sold nor distributed without //~ this header and disclaimer.
//~ Requirements for both RLV Relay Script (RLVRS) and in-world objects //~ preset channel with llSay only, no llWhisper no llShout //~ preset protocol : cmd_name,user_uuid,@behav=param (behav and param parts are *lowercase*) //~ preset replies (*lowercase*) : ok|ko|<version> (version only when receiving a "version" meta-command)
//~ Special requirements for the Relay Script //~ send the exact @behav=param part in an llOwnerSay, without any change whatsoever //~ retain list of restrictions and their sources for relog, force sit if unsit is prevented //~ refresh (purge restrictions of which the source is not around anymore) //~ name of script contains RLVnnn where nnn is the minimal version it is compatible with (ex : RLV110), the user must have access to that version (dialog, message, object name...) to check everything works correctly //~ implement some security (group, distance, length of message, deny "force" commands...) //~ implement some user-friendliness (authorizations, menus, level of control (accept "force" commands y/n ?)... )
//~ Special requirements for the in-world objects //~ never rely on an answer from the RLVRS, requests can be denied silently, the relay can be unworn, the avatar can tp out or crash... => use timeouts //~ don't poll the dataserver for online status, the RLVRS takes care of the relog part //~ avoid unnecessary messages, do not spam the user if they are not using RLV //~ do not rely on RLV to function, as it is only an enhancement not a requirement //~ try to leave the user's RLVRS clean of restrictions when the game is over (don't make them use Refresh, lift restrictions properly)
string RLVRS_PROTOCOL_VERSION = "1000";
integer RLVRS_CHANNEL = -1812221819; //RLVRS in numbers integer DIALOG_CHANNEL = -1812220409; //RLVDI in numbers
list lRestrictions; // restrictions currently applied (without the "=n" part)
key kSource; // UUID of the object I'm commanded by, always equal to NULL_KEY if lRestrictions is empty, always set if not string sPendingName; // name of initiator of pending request (first request of a session in mode 1) key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1) string sPendingMessage; // message of pending request (first request of a session in mode 1) integer nMode; // 0:off, 1:accept on authorization, 2:accept all
Ack (string cmd_id, key id, string ack) { // acknowledge or reject
llSay (RLVRS_CHANNEL, cmd_id+","+(string)id+","+ack);
}
SendRLCmd (string cmd) { // cmd begins with a '@'
llOwnerSay (cmd);
}
integer IsSimpleRequest (string cmd) {
// cmd ends with "=" and a number (@version, @getoutfit, @getattach) integer ind=llSubStringIndex (cmd, "="); if (ind>-1) { string param=llGetSubString (cmd, ind+1, -1); if ((integer)param!=0 || param=="0") return 1; } return 0;
}
Execute (string name, key id, string message) {
// execute a non-parsed command, we are sure the message has been accepted regarding its source (authorization, on/off) // this command can still be denied, but this time there will be an acknowledgement list tokens=llParseString2List (message, [",", "="], []); if (llGetListLength (tokens)==4) { // this is a normal command string cmd_id=llList2String (tokens, 0); // CheckAttach key target=llList2Key (tokens, 1); // UUID if (target==llGetOwner ()) { // talking to me ? string behav=llList2String (tokens, 2); // @getattach:skull string param=llList2String (tokens, 3); // 2222 key my_parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0); key its_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0); key owner_key=llGetOwnerKey (id); // do the actual check if (owner_key==llGetOwner () // IF I am the owner of the object || its_group==my_parcel_group // OR its group is the same as the parcel I'm on ) { // command accepted, check the param, add to list if needed, pass to viewer, acknowledge integer ind=llListFindList (lRestrictions, [behav]); if (param=="n" || param=="add") { // add to lRestrictions if (ind<0) lRestrictions+=[behav]; kSource=id; } else if (param=="y" || param=="rem") { // remove from lRestrictions if (ind>-1) lRestrictions=llDeleteSubList (lRestrictions, ind, ind); if (llGetListLength (lRestrictions)==0) kSource=NULL_KEY; } SendRLCmd (behav+"="+param); // execute command Ack (cmd_id, id, "ok"); // acknowledge } else { // command denied, reply "request denied" Ack (cmd_id, id, "ko"); } } } else if (llGetListLength (tokens)==3) { // meta-command, not relayed to the viewer (release, get protocol version) string cmd_id=llList2String (tokens, 0); // CheckAttach key target=llList2Key (tokens, 1); // UUID string behav=llList2String (tokens, 2); if (target==llGetOwner ()) { // talking to me ? if (behav=="version") { // checking relay version Ack (cmd_id, id, RLVRS_PROTOCOL_VERSION); } else if (behav=="release") { // release all the restrictions (end session) ReleaseRestrictions (); Ack (cmd_id, id, "ok"); } } }
}
ReleaseRestrictions () {
kSource=NULL_KEY; integer i; integer len=llGetListLength (lRestrictions); for (i=0; i<len; ++i) { SendRLCmd (llList2String (lRestrictions, i)+"=y"); }
}
string GetMode () {
if (nMode==0) return "RLV Relay is OFF"; else if (nMode==1) return "RLV Relay is ON (permission needed)"; else return "RLV Relay is ON (auto-accept)";
}
Init () {
nMode=1; kSource=NULL_KEY; lRestrictions=[]; sPendingId=NULL_KEY; sPendingName=""; sPendingMessage=""; llListen (RLVRS_CHANNEL, "", "", ""); llListen (DIALOG_CHANNEL, "", llGetOwner (), ""); llOwnerSay (GetMode ());
}
default
{
state_entry () { Init (); } on_rez(integer start_param) { if (nMode) { integer i; integer len=llGetListLength (lRestrictions); string restr; for (i=0; i<len; ++i) { restr=llList2String (lRestrictions, i); SendRLCmd (restr+"=n"); if (restr=="@unsit") { SendRLCmd ("@sit:"+(string)kSource+"=force"); } } } llOwnerSay (GetMode ()); } listen(integer channel, string name, key id, string message) { // CheckAttach,UUID,@getattach:skull=2222 if (channel==RLVRS_CHANNEL) { // do a basic check without parsing the command, reject without any acknowledgement when needed if (nMode==0) return; // mode is 0 (off) => reject if (kSource!=NULL_KEY && kSource!=id) return; // already used by another object => reject if (IsSimpleRequest (message)) { // simple harmless command such as @version, @getoutfit or @getattach Execute (name, id, message); return; } if (nMode==1) { if (kSource==NULL_KEY) { if (sPendingId==NULL_KEY) { // not under operation yet, prompt the user, delay reply until they accept or reject sPendingId=id; sPendingName=name; sPendingMessage=message; llDialog (llGetOwner (), name+" would like to connect to your RLV Relay and send commands to your viewer.\n\nDo you accept ?", ["Yes", "No"], DIALOG_CHANNEL); } } else if (kSource==id) { // already operated by this object, accept automatically Execute (name, id, message); } } else if (nMode==2 && (kSource==NULL_KEY || kSource==id)) { // accept automatically Execute (name, id, message); } } else if (channel==DIALOG_CHANNEL) { if (sPendingId!=NULL_KEY) { if (message=="Yes") { // pending request authorized => process it Execute (sPendingName, sPendingId, sPendingMessage); } else if (sPendingId!=NULL_KEY && message=="No") { // denied => do nothing at all } // clear pending request sPendingName=""; sPendingId=NULL_KEY; sPendingMessage=""; } } } touch_start(integer num_detected) { key toucher=llDetectedKey (0); if (toucher==llGetOwner ()) { ++nMode; if (nMode>2) nMode=0; if (nMode==0) ReleaseRestrictions (); llOwnerSay (GetMode ()); } }
changed(integer change) { if (change & CHANGED_OWNER) llResetScript (); }
}
</lsl>