Subject: [vwrap] VWRAP example client code

From: "Hurliman, John" <[email-masked]>
Date: Thu, 8 Apr 2010 11:13:31 -0600

I was trying out Unity3D last night to see if I could build a simple VWRAP client. I don't have a full client working, but I did hack together a first pass at the VWRAP login implementation. I'm posting the code here to get the ball rolling on the VWRAP teleport discussion and shine some light on a few cloudy issues.

Notes:

* This code was designed assuming the launch document contains a pre-authenticated login URL. Pre-authenticated login URLs are actually kind of silly; if you are going that route why not just embed the agent seed capability right in the launch doc and skip draft-hamrick-vwrap-authentication entirely. This sticks closer to the VWRAP drafts as published though.

* Speaking of steps you can skip, the agent/session capability only seems relevant to platforms using LLUDP. This code is trying to get a websocket_event_queue capability which has the session token embedded in the websocket URL, so there's no need for a session_id or secure_session_id. I included the code to fetch agent/session just to stick with what is currently published in OGP Teleport Draft 5, and in case I missed some obvious use case where a separate session_id token is needed.

* Unity3D makes heavy use of coroutines for things like fetching web resources, and since coroutines are far from optimal in C# (supported at the compiler level instead of the runtime, no way to return values or use "out" parameters with coroutines) it uses closures to return values from PostLLSD() and GetLLSD(). Apologies if the logic is difficult to follow there.

* If you look closely you'll notice I just invented a seed capability format. It is sending { "cap1": {},  "cap2": {} } and receiving { "cap1": {"uri": "..."}, "cap2": {"uri": "..."} }. I think this captures the spirit of the "map of maps" concept that was discussed at IETF77, although nothing has been discussed for the specific format yet.

John

using UnityEngine;
using System;
using System.Collections;
using System.Text;
using OpenMetaverse;
using OpenMetaverse.StructuredData;

public class VWRAP : MonoBehaviour
{
    public static bool TryParseLaunchDoc(string document, out Uri loginUri, out Uri regionUri)
    {
        OSDMap launchDoc = OSDParser.Deserialize(document) as OSDMap;

        if (launchDoc != null)
        {
            loginUri = launchDoc["loginuri"].AsUri();
            regionUri = launchDoc["region"].AsUri();
        }
        else
        {
            loginUri = null;
            regionUri = null;
        }

        return loginUri != null && regionUri != null;
    }

    public IEnumerator PreAuthenticatedLogin(Uri loginUri, Uri regionUri)
    {
        OSDMap request;
        OSDMap response = null;

        // 1. POST to the pre-authenticated loginUri and receive an agent_seed_capability
        request = new OSDMap();
        yield return StartCoroutine(PostLLSD(loginUri.ToString(), request, delegate(OSDMap ret) { response = ret; }));
        if (response == null || !response.ContainsKey("agent_seed_capability"))
        {
            DebugConsole.Log("Login failed POSTing to loginuri " + loginUri + ", response=\"" + response + "\"");
            yield break;
        }
        string agentSeedCapUrl = response["agent_seed_capability"].AsString();

        // 2. POST to the agent_seed_capability and get a list of capabilities
        request = new OSDMap();
        request["agent/info"] = new OSDMap();
        request["agent/session"] = new OSDMap();
        yield return StartCoroutine(PostLLSD(agentSeedCapUrl, request, delegate(OSDMap ret) { response = ret; }));
        if (response == null || !(response["agent/info"] is OSDMap) || !(response["agent/session"] is OSDMap))
        {
            DebugConsole.Log("Login failed POSTing to agent_seed_capability " + agentSeedCapUrl + ", response=\"" + response + "\"");
            yield break;
        }
        string agentInfoUrl = ((OSDMap)response["agent/info"])["uri"].AsString();
        string agentSessionUrl = ((OSDMap)response["agent/session"])["uri"].AsString();

        // 3. GET agent/info to get our agent_id, name, and home/last locations
        yield return StartCoroutine(GetLLSD(agentInfoUrl, delegate(OSDMap ret) { response = ret; }));
        if (response == null || !response.ContainsKey("agent_id"))
        {
            DebugConsole.Log("Login failed POSTing to agent/info " + agentSeedCapUrl + ", response=\"" + response + "\"");
            yield break;
        }
        UUID agentID = response["agent_id"].AsUUID();
        string name = response["name"].AsString();

        // 4. GET agent/session to get our session_id and secure_session_id
        yield return StartCoroutine(GetLLSD(agentSessionUrl, delegate(OSDMap ret) { response = ret; }));
        if (response == null || !response.ContainsKey("session_id"))
        {
            DebugConsole.Log("Login failed POSTing to agent/session " + agentSeedCapUrl + ", response=\"" + response + "\"");
            yield break;
        }
        UUID sessionID = response["session_id"].AsUUID();

        // 5. POST to the regionUri (public region seed capability) to get the rez_avatar/place capability
        request = new OSDMap();
        request["rez_avatar/place"] = new OSDMap();
        yield return StartCoroutine(PostLLSD(regionUri.ToString(), request, delegate(OSDMap ret) { response = ret; }));
        if (response == null || !(response["rez_avatar/place"] is OSDMap))
        {
            DebugConsole.Log("Login failed POSTing to public region seed capabability " + regionUri + ", response=\"" + response + "\"");
            yield break;
        }
        string rezAvatarPlaceUrl = ((OSDMap)response["rez_avatar/place"])["uri"].AsString();

        // 6. POST to rez_avatar/place to get the region_seed_capability
        request = new OSDMap();
        request["public_region_seed_capability"] = OSD.FromUri(regionUri);
        // TODO: request["position"] = OSD.FromVector3(...);
        yield return StartCoroutine(PostLLSD(rezAvatarPlaceUrl, request, delegate(OSDMap ret) { response = ret; }));
        if (response == null || !response["connect"].AsBoolean() || !response.ContainsKey("region_seed_capability"))
        {
            DebugConsole.Log("Login failed POSTing to rez_avatar/place " + rezAvatarPlaceUrl + ", response=\"" + response + "\"");
            yield break;
        }
        string regionSeedCapUrl = response["region_seed_capability"].AsString();

        // 7. POST to region_seed_capability to get the region capabilities
        request = new OSDMap();
        request["websocket_event_queue"] = new OSDMap();
        yield return StartCoroutine(PostLLSD(regionSeedCapUrl, request, delegate(OSDMap ret) { response = ret; }));
        if (response == null || !(response["websocket_event_queue"] is OSDMap))
        {
            DebugConsole.Log("Login failed POSTing to region_seed_capability " + regionSeedCapUrl + ", response=\"" + response + "\"");
            yield break;
        }
        string websocketEventQueue = ((OSDMap)response["websocket_event_queue"])["uri"].AsString();

        // FIXME: Return the data we collected
    }

    public static IEnumerator PostLLSD(string url, OSDMap requestMap, Action<OSDMap> callback)
    {
        byte[] requestData = Encoding.UTF8.GetBytes(OSDParser.SerializeJsonString(requestMap));

        Hashtable headers = new Hashtable();
        headers["Content-Type"] = "application/json+llsd";

        WWW request = new WWW(url, requestData, headers);
        
        yield return request;

        if (!String.IsNullOrEmpty(request.error))
            DebugConsole.Log("Error posting data to " + url + ": " + request.error);

        if (request.bytes != null && request.bytes.Length > 0)
        {
            OSDMap responseMap = OSDParser.Deserialize(request.bytes) as OSDMap;
            callback(responseMap);
        }
        else
        {
            callback(null);
        }
    }

    public static IEnumerator GetLLSD(string url, Action<OSDMap> callback)
    {
        WWW request = new WWW(url);

        yield return request;

        if (!String.IsNullOrEmpty(request.error))
            DebugConsole.Log("Error posting data to " + url + ": " + request.error);

        if (request.bytes != null && request.bytes.Length > 0)
        {
            OSDMap responseMap = OSDParser.Deserialize(request.bytes) as OSDMap;
            callback(responseMap);
        }
        else
        {
            callback(null);
        }
    }
}