/*
* Copyright (c) 2006, Second Life Reverse Engineering Team
* All rights reserved.
*
* - Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* - Neither the name of the Second Life Reverse Engineering Team nor the names
* of its contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Timers;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Net;
using System.Net.Sockets;
using System.Globalization;
using System.IO;
using Nwc.XmlRpc;
using Nii.JSON;
using libsecondlife.Packets;
namespace libsecondlife
{
///
/// This exception is thrown whenever a network operation is attempted
/// without a network connection.
///
public class NotConnectedException : ApplicationException { }
///
/// Capabilities is the name of the bi-directional HTTP REST protocol that
/// Second Life uses to communicate transactions such as teleporting or
/// asset transfers
///
public class Caps
{
///
/// Triggered when an event is received via the EventQueueGet capability;
///
///
///
public delegate void EventQueueCallback(string message, object body);
/// Reference to the SecondLife client this system is connected to
public SecondLife Client;
/// Reference to the simulator this system is connected to
public Simulator Simulator;
///
public string SeedCapsURI { get { return Seedcaps; } }
internal bool Dead = false;
internal string Seedcaps;
private StringDictionary Capabilities = new StringDictionary();
private string EventQueueCap = String.Empty;
private Thread EventQueueThread;
private WebRequest EventQueueRequest = null;
private List Callbacks;
///
/// Default constructor
///
///
///
///
///
internal Caps(SecondLife client, Simulator simulator, string seedcaps, List callbacks)
{
Client = client;
Simulator = simulator;
Seedcaps = seedcaps;
Callbacks = callbacks;
ArrayList req = new ArrayList();
req.Add("MapLayer");
req.Add("MapLayerGod");
req.Add("NewAgentInventory");
req.Add("EventQueueGet");
byte[] data = LLSD.LLSDSerialize(req);
WebRequest request = WebRequest.Create(seedcaps);
request.Method = "POST";
request.ContentLength = data.Length;
Stream reqStream = request.GetRequestStream();
reqStream.Write(data, 0, data.Length);
reqStream.Close();
request.BeginGetResponse(new AsyncCallback(InitialResponse), request);
}
public void Disconnect()
{
Dead = true;
if (EventQueueRequest != null)
EventQueueRequest.Abort();
}
private void RunEventQueue()
{
// Make a new request
Hashtable req = new Hashtable();
req["ack"] = null;
req["done"] = false;
byte[] data = LLSD.LLSDSerialize(req);
EventQueueRequest = WebRequest.Create(EventQueueCap);
EventQueueRequest.Method = "POST";
EventQueueRequest.ContentLength = data.Length;
Stream reqStream = EventQueueRequest.GetRequestStream();
reqStream.Write(data, 0, data.Length);
reqStream.Close();
EventQueueRequest.BeginGetResponse(new AsyncCallback(EventQueueResponse), EventQueueRequest);
}
private void InitialResponse(IAsyncResult result)
{
WebRequest request = (WebRequest)result.AsyncState;
byte[] buffer = null;
try
{
HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);
BinaryReader reader = new BinaryReader(response.GetResponseStream());
buffer = reader.ReadBytes((int)response.ContentLength);
response.Close();
}
catch (WebException e)
{
Client.Log("CAPS probe error: " + e.Message, Helpers.LogLevel.Warning);
}
if (buffer != null)
{
Hashtable resp = (Hashtable)LLSD.LLSDDeserialize(buffer);
foreach (string cap in resp.Keys)
{
Client.DebugLog(String.Format("Got cap {0}: {1}", cap, (string)resp[cap]));
Capabilities[cap] = (string)resp[cap];
}
if (Capabilities.ContainsKey("EventQueueGet"))
{
EventQueueCap = Capabilities["EventQueueGet"];
Client.Log("Running event queue", Helpers.LogLevel.Info);
EventQueueThread = new Thread(new ThreadStart(RunEventQueue));
EventQueueThread.Start();
}
}
}
private void EventQueueResponse(IAsyncResult result)
{
byte[] buffer = null;
long ack = 0;
try
{
HttpWebResponse response = (HttpWebResponse)EventQueueRequest.EndGetResponse(result);
BinaryReader reader = new BinaryReader(response.GetResponseStream());
buffer = reader.ReadBytes((int)response.ContentLength);
response.Close();
}
catch (WebException e)
{
Client.DebugLog("EventQueue: " + e.Message);
}
if (buffer != null)
{
Hashtable resp = (Hashtable)LLSD.LLSDDeserialize(buffer);
ArrayList events = (ArrayList)resp["events"];
ack = (long)resp["id"];
foreach (Hashtable evt in events)
{
string msg = (string)evt["message"];
object body = (object)evt["body"];
Client.DebugLog("Event " + msg + ":" + Environment.NewLine + LLSD.LLSDDump(body, 0));
if (!Dead)
{
foreach (EventQueueCallback callback in Callbacks)
{
try { callback(msg, body); }
catch (Exception e) { Client.Log(e.ToString(), Helpers.LogLevel.Error); }
}
}
}
}
if (!Dead)
{
// Make a new request
Hashtable req = new Hashtable();
if (ack != 0) req["ack"] = ack;
else req["ack"] = null;
req["done"] = false;
byte[] data = LLSD.LLSDSerialize(req);
EventQueueRequest = WebRequest.Create(EventQueueCap);
EventQueueRequest.Method = "POST";
EventQueueRequest.ContentLength = data.Length;
Stream reqStream = EventQueueRequest.GetRequestStream();
reqStream.Write(data, 0, data.Length);
reqStream.Close();
EventQueueRequest.BeginGetResponse(new AsyncCallback(EventQueueResponse), EventQueueRequest);
}
}
}
///
/// NetworkManager is responsible for managing the network layer of
/// libsecondlife. It tracks all the server connections, serializes
/// outgoing traffic and deserializes incoming traffic, and provides
/// instances of delegates for network-related events.
///
public class NetworkManager
{
///
/// Explains why a simulator or the grid disconnected from us
///
public enum DisconnectType
{
/// The client requested the logout or simulator disconnect
ClientInitiated,
/// The server notified us that it is disconnecting
ServerInitiated,
/// Either a socket was closed or network traffic timed out
NetworkTimeout
}
///
/// Coupled with RegisterCallback(), this is triggered whenever a packet
/// of a registered type is received
///
///
///
public delegate void PacketCallback(Packet packet, Simulator simulator);
///
/// Triggered when a simulator other than the simulator that is currently
/// being occupied disconnects for whatever reason
///
/// The simulator that disconnected, which will become a null
/// reference after the callback is finished
/// Enumeration explaining the reason for the disconnect
public delegate void SimDisconnectedCallback(Simulator simulator, DisconnectType reason);
///
/// Triggered when we are logged out of the grid due to a simulator request,
/// client request, network timeout, or any other cause
///
/// Enumeration explaining the reason for the disconnect
/// If we were logged out by the simulator, this
/// is a message explaining why
public delegate void DisconnectedCallback(DisconnectType reason, string message);
///
/// Triggered when CurrentSim changes
///
/// A reference to the old value of CurrentSim
public delegate void CurrentSimChangedCallback(Simulator PreviousSimulator);
///
/// An event for the connection to a simulator other than the currently
/// occupied one disconnecting
///
public event SimDisconnectedCallback OnSimDisconnected;
///
/// An event for being logged out either through client request, server
/// forced, or network error
///
public event DisconnectedCallback OnDisconnected;
///
/// An event for when CurrentSim changes
///
public event CurrentSimChangedCallback OnCurrentSimChanged;
/// The permanent UUID for the logged in avatar
public LLUUID AgentID = LLUUID.Zero;
/// Temporary UUID assigned to this session, used for
/// verifying our identity in packets
public LLUUID SessionID = LLUUID.Zero;
/// Shared secret UUID that is never sent over the wire
public LLUUID SecureSessionID = LLUUID.Zero;
/// Uniquely identifier associated with our connections to
/// simulators
public uint CircuitCode;
/// String holding a descriptive error on login failure, empty
/// otherwise
public string LoginError = String.Empty;
/// The simulator that the logged in avatar is currently
/// occupying
public Simulator CurrentSim = null;
/// The capabilities for the current simulator
public Caps CurrentCaps = null;
/// The complete dictionary of all the login values returned
/// by the RPC login server, converted to native data types wherever
/// possible
public Dictionary LoginValues = new Dictionary();
///
/// Shows whether the network layer is logged in to the grid or not
///
public bool Connected { get { return connected; } }
///
internal Dictionary> Callbacks = new Dictionary>();
private SecondLife Client;
private List Simulators = new List();
private System.Timers.Timer DisconnectTimer;
private System.Timers.Timer LogoutTimer;
private bool connected = false;
private List EventQueueCallbacks = new List();
private ManualResetEvent LogoutReplyEvent = new ManualResetEvent(false);
///
/// Default constructor
///
/// Reference to the SecondLife client
public NetworkManager(SecondLife client)
{
Client = client;
CurrentSim = null;
// Register the internal callbacks
RegisterCallback(PacketType.RegionHandshake, new PacketCallback(RegionHandshakeHandler));
RegisterCallback(PacketType.StartPingCheck, new PacketCallback(StartPingCheckHandler));
RegisterCallback(PacketType.ParcelOverlay, new PacketCallback(ParcelOverlayHandler));
RegisterCallback(PacketType.EnableSimulator, new PacketCallback(EnableSimulatorHandler));
RegisterCallback(PacketType.KickUser, new PacketCallback(KickUserHandler));
RegisterCallback(PacketType.LogoutReply, new PacketCallback(LogoutReplyHandler));
// The proper timeout for this will get set at Login
DisconnectTimer = new System.Timers.Timer();
DisconnectTimer.Elapsed += new ElapsedEventHandler(DisconnectTimer_Elapsed);
}
///
/// Register an event handler for a packet. This is a low level event
/// interface and should only be used if you are doing something not
/// supported in libsecondlife
///
/// Packet type to trigger events for
/// Callback to fire when a packet of this type
/// is received
public void RegisterCallback(PacketType type, PacketCallback callback)
{
if (!Callbacks.ContainsKey(type))
{
Callbacks[type] = new List();
}
List callbackArray = Callbacks[type];
callbackArray.Add(callback);
}
///
/// Unregister an event handler for a packet. This is a low level event
/// interface and should only be used if you are doing something not
/// supported in libsecondlife
///
/// Packet type this callback is registered with
/// Callback to stop firing events for
public void UnregisterCallback(PacketType type, PacketCallback callback)
{
if (!Callbacks.ContainsKey(type))
{
Client.Log("Trying to unregister a callback for packet " + type.ToString() +
" when no callbacks are setup for that packet", Helpers.LogLevel.Info);
return;
}
List callbackArray = Callbacks[type];
if (callbackArray.Contains(callback))
{
callbackArray.Remove(callback);
}
else
{
Client.Log("Trying to unregister a non-existant callback for packet " + type.ToString(),
Helpers.LogLevel.Info);
}
}
///
/// Register a CAPS event handler
///
/// Callback to fire when a CAPS event is received
public void RegisterEventCallback(Caps.EventQueueCallback callback)
{
EventQueueCallbacks.Add(callback);
}
///
/// Send a packet to the simulator the avatar is currently occupying
///
/// Packet to send
public void SendPacket(Packet packet)
{
if (CurrentSim != null && CurrentSim.Connected)
CurrentSim.SendPacket(packet, true);
}
///
/// Send a packet to a specified simulator
///
/// Packet to send
/// Simulator to send the packet to
public void SendPacket(Packet packet, Simulator simulator)
{
if (simulator != null)
simulator.SendPacket(packet, true);
}
///
/// Send a raw byte array as a packet to the current simulator
///
/// Byte array containing a packet
/// Whether to set the second, third, and fourth
/// bytes of the payload to the current sequence number
public void SendPacket(byte[] payload, bool setSequence)
{
if (CurrentSim != null)
CurrentSim.SendPacket(payload, setSequence);
}
///
/// Send a raw byte array as a packet to the specified simulator
///
/// Byte array containing a packet
/// Simulator to send the packet to
/// Whether to set the second, third, and fourth
/// bytes of the payload to the current sequence number
public void SendPacket(byte[] payload, Simulator simulator, bool setSequence)
{
if (simulator != null)
simulator.SendPacket(payload, setSequence);
}
///
/// Build a start location URI for passing to the Login function
///
/// Name of the simulator to start in
/// X coordinate to start at
/// Y coordinate to start at
/// Z coordinate to start at
/// String with a URI that can be used to login to a specified
/// location
public static string StartLocation(string sim, int x, int y, int z)
{
// uri:sim name&x&y&z
return "uri:" + sim.ToLower() + "&" + x + "&" + y + "&" + z;
}
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
public Dictionary DefaultLoginValues(string firstName, string lastName,
string password, string mac, string startLocation, int major, int minor, int patch,
int build, string platform, string viewerDigest, string userAgent, string author,
bool md5pass)
{
Dictionary values = new Dictionary();
values["first"] = firstName;
values["last"] = lastName;
values["passwd"] = md5pass ? password : Helpers.MD5(password);
values["start"] = startLocation;
values["major"] = major;
values["minor"] = minor;
values["patch"] = patch;
values["build"] = build;
values["platform"] = platform;
values["mac"] = mac;
values["agree_to_tos"] = "true";
values["read_critical"] = "true";
values["viewer_digest"] = viewerDigest;
values["user-agent"] = userAgent + " (" + Client.Settings.VERSION + ")";
values["author"] = author;
// Build the options array
List