/*
* 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.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Globalization;
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 { }
///
/// Simulator is a wrapper for a network connection to a simulator and the
/// Region class representing the block of land in the metaverse.
///
public class Simulator
{
/// A public reference to the client that this Simulator object
/// is attached to
public SecondLife Client;
/// The maximum size of the sequence number inbox, used to
/// check for resent and/or duplicate packets
public const int INBOX_SIZE = 10000;
/// The Region class that this Simulator wraps
public Region Region;
///
/// The ID number associated with this particular connection to the
/// simulator, used to emulate TCP connections. This is used
/// internally for packets that have a CircuitCode field.
///
public uint CircuitCode
{
get { return circuitCode; }
set { circuitCode = value; }
}
///
/// The IP address and port of the server.
///
public IPEndPoint IPEndPoint
{
get { return ipEndPoint; }
}
///
/// A boolean representing whether there is a working connection to the
/// simulator or not.
///
public bool Connected
{
get { return connected; }
}
///
/// Used internally to track sim disconnections, do not modify this
/// variable.
///
public bool DisconnectCandidate = false;
private NetworkManager Network;
private Dictionary> Callbacks;
private ushort Sequence = 0;
private byte[] RecvBuffer = new byte[4096];
private byte[] ZeroBuffer = new byte[8192];
private byte[] ZeroOutBuffer = new byte[4096];
private Socket Connection = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
private AsyncCallback ReceivedData;
private Dictionary NeedAck = new Dictionary();
private Queue Inbox = new Queue(INBOX_SIZE);
private List PendingAcks = new List();
private bool connected = false;
private uint circuitCode;
private IPEndPoint ipEndPoint;
private EndPoint endPoint;
private System.Timers.Timer AckTimer;
// Every tick, all ACKs are sent out and the age of unACKed packets is checked
private int TickLength = 500;
// Number of milliseconds before a packet is assumed lost and resent
private int ResendTimeout = 4000;
private const int SimConnectTimeout = 15000;
///
///
///
///
///
///
///
///
public Simulator(SecondLife client, Dictionary> callbacks,
uint circuit, IPAddress ip, int port)
{
Client = client;
Network = client.Network;
Callbacks = callbacks;
Region = new Region(client);
circuitCode = circuit;
AckTimer = new System.Timers.Timer(TickLength);
AckTimer.Elapsed += new ElapsedEventHandler(AckTimer_Elapsed);
// Initialize the callback for receiving a new packet
ReceivedData = new AsyncCallback(OnReceivedData);
Client.Log("Connecting to " + ip.ToString() + ":" + port, Helpers.LogLevel.Info);
try
{
// Create an endpoint that we will be communicating with (need it in two
// types due to .NET weirdness)
ipEndPoint = new IPEndPoint(ip, port);
endPoint = (EndPoint)ipEndPoint;
// Associate this simulator's socket with the given ip/port and start listening
Connection.Connect(endPoint);
Connection.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref endPoint, ReceivedData, null);
// Send the UseCircuitCode packet to initiate the connection
UseCircuitCodePacket use = new UseCircuitCodePacket();
use.CircuitCode.Code = circuitCode;
use.CircuitCode.ID = Network.AgentID;
use.CircuitCode.SessionID = Network.SessionID;
// Start the ACK timer
AckTimer.Start();
// Send the initial packet out
SendPacket(use, true);
// Track the current time for timeout purposes
int start = Environment.TickCount;
while (true)
{
if (connected || Environment.TickCount - start > SimConnectTimeout)
{
return;
}
System.Threading.Thread.Sleep(10);
}
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
Client.Log(e.ToString(), Helpers.LogLevel.Error);
}
}
///
///
///
public void Disconnect()
{
// Send the CloseCircuit notice
CloseCircuitPacket close = new CloseCircuitPacket();
try
{
Connection.Send(close.ToBytes());
}
catch (SocketException)
{
// There's a high probability of this failing if the network is
// disconnected, so don't even bother logging the error
}
try
{
// Shut the socket communication down
Connection.Shutdown(SocketShutdown.Both);
}
catch (SocketException e)
{
Client.Log(e.ToString(), Helpers.LogLevel.Error);
}
connected = false;
}
///
///
///
///
///
public void SendPacket(Packet packet, bool incrementSequence)
{
byte[] buffer;
int bytes;
if (!connected && packet.Type != PacketType.UseCircuitCode)
{
Client.Log("Trying to send a " + packet.Type.ToString() + " packet when the socket is closed",
Helpers.LogLevel.Warning);
throw new NotConnectedException();
}
if (packet.Header.AckList.Length > 0)
{
// Scrub any appended ACKs since all of the ACK handling is done here
packet.Header.AckList = new uint[0];
}
packet.Header.AppendedAcks = false;
if (incrementSequence)
{
// Set the sequence number
if (Sequence == ushort.MaxValue)
Sequence = 0;
else
Sequence++;
packet.Header.Sequence = Sequence;
if (packet.Header.Reliable)
{
// Keep track of when this packet was sent out
packet.TickCount = Environment.TickCount;
lock (NeedAck)
{
if (!NeedAck.ContainsKey(packet.Header.Sequence))
{
NeedAck.Add(packet.Header.Sequence, packet);
}
else
{
Client.Log("Attempted to add a duplicate sequence number (" +
packet.Header.Sequence + ") to the NeedAck dictionary for packet type " +
packet.Type.ToString(), Helpers.LogLevel.Warning);
}
}
// Append any ACKs that need to be sent out to this packet
lock (PendingAcks)
{
if (PendingAcks.Count > 0 && packet.Type != PacketType.PacketAck &&
packet.Type != PacketType.LogoutRequest)
{
packet.Header.AckList = new uint[PendingAcks.Count];
int i = 0;
foreach (uint ack in PendingAcks)
{
packet.Header.AckList[i] = ack;
i++;
}
PendingAcks.Clear();
packet.Header.AppendedAcks = true;
}
}
}
}
// Serialize the packet
buffer = packet.ToBytes();
bytes = buffer.Length;
try
{
// Zerocode if needed
if (packet.Header.Zerocoded)
{
lock (ZeroOutBuffer)
{
bytes = Helpers.ZeroEncode(buffer, bytes, ZeroOutBuffer);
Connection.Send(ZeroOutBuffer, bytes, SocketFlags.None);
}
}
else
{
Connection.Send(buffer, bytes, SocketFlags.None);
}
}
catch (SocketException e)
{
Client.Log(e.ToString(), Helpers.LogLevel.Error);
// FIXME: Assume this socket is dead and Disconnect()
}
}
///
///
///
///
public void SendPacket(byte[] payload)
{
if (!connected)
{
throw new NotConnectedException();
}
try
{
Connection.Send(payload, payload.Length, SocketFlags.None);
}
catch (SocketException e)
{
Client.Log(e.ToString(), Helpers.LogLevel.Error);
}
}
public override string ToString()
{
return Region.Name + " (" + ipEndPoint.ToString() + ")";
}
private void SendAck(ushort id)
{
PacketAckPacket ack = new PacketAckPacket();
ack.Packets = new PacketAckPacket.PacketsBlock[1];
ack.Packets[0] = new PacketAckPacket.PacketsBlock();
ack.Packets[0].ID = id;
ack.Header.Reliable = false;
lock (PendingAcks)
{
if (PendingAcks.Contains(id))
{
PendingAcks.Remove(id);
}
}
SendPacket(ack, true);
}
private void SendAcks()
{
lock (PendingAcks)
{
if (PendingAcks.Count > 0)
{
int i = 0;
PacketAckPacket acks = new PacketAckPacket();
acks.Packets = new PacketAckPacket.PacketsBlock[PendingAcks.Count];
foreach (uint ack in PendingAcks)
{
acks.Packets[i] = new PacketAckPacket.PacketsBlock();
acks.Packets[i].ID = ack;
i++;
}
acks.Header.Reliable = false;
SendPacket(acks, true);
PendingAcks.Clear();
}
}
}
private void ResendUnacked()
{
int now = Environment.TickCount;
lock (NeedAck)
{
foreach (Packet packet in NeedAck.Values)
{
if (now - packet.TickCount > ResendTimeout)
{
Client.Log("Resending " + packet.Type.ToString() + " packet, " +
(now - packet.TickCount) + "ms have passed", Helpers.LogLevel.Info);
packet.Header.Resent = true;
SendPacket(packet, false);
}
}
}
}
private void OnReceivedData(IAsyncResult result)
{
Packet packet = null;
int numBytes;
// If we're receiving data the sim connection is open
connected = true;
// Update the disconnect flag so this sim doesn't time out
DisconnectCandidate = false;
lock (RecvBuffer)
{
// Retrieve the incoming packet
try
{
numBytes = Connection.EndReceiveFrom(result, ref endPoint);
int packetEnd = numBytes - 1;
packet = Packet.BuildPacket(RecvBuffer, ref packetEnd, ZeroBuffer);
Connection.BeginReceiveFrom(RecvBuffer, 0, RecvBuffer.Length, SocketFlags.None, ref endPoint, ReceivedData, null);
}
catch (SocketException)
{
Client.Log(endPoint.ToString() + " socket is closed, shutting down " + this.Region.Name,
Helpers.LogLevel.Info);
connected = false;
Network.DisconnectSim(this);
return;
}
}
// Fail-safe check
if (packet == null)
{
Client.Log("Couldn't build a message from the incoming data", Helpers.LogLevel.Warning);
return;
}
// Track the sequence number for this packet if it's marked as reliable
if (packet.Header.Reliable)
{
if (PendingAcks.Count > 10)
{
SendAcks();
}
// Check if we already received this packet
if (Inbox.Contains(packet.Header.Sequence))
{
Client.Log("Received a duplicate " + packet.Type.ToString() + ", sequence=" +
packet.Header.Sequence + ", resent=" + ((packet.Header.Resent) ? "Yes" : "No") +
", Inbox.Count=" + Inbox.Count + ", NeedAck.Count=" + NeedAck.Count,
Helpers.LogLevel.Info);
// Send an ACK for this packet immediately
SendAck(packet.Header.Sequence);
// Avoid firing a callback twice for the same packet
return;
}
else
{
lock (PendingAcks)
{
PendingAcks.Add((uint)packet.Header.Sequence);
}
}
}
// Add this packet to our inbox
lock (Inbox)
{
if (Inbox.Count >= INBOX_SIZE)
{
Inbox.Dequeue();
}
Inbox.Enqueue(packet.Header.Sequence);
}
// Handle appended ACKs
if (packet.Header.AppendedAcks)
{
lock (NeedAck)
{
foreach (ushort ack in packet.Header.AckList)
{
NeedAck.Remove(ack);
}
}
}
// Handle PacketAck packets
if (packet.Type == PacketType.PacketAck)
{
PacketAckPacket ackPacket = (PacketAckPacket)packet;
lock (NeedAck)
{
foreach (PacketAckPacket.PacketsBlock block in ackPacket.Packets)
{
NeedAck.Remove((ushort)block.ID);
}
}
}
// Fire the registered packet events
#region FireCallbacks
try
{
if (Callbacks.ContainsKey(packet.Type))
{
List callbackArray = Callbacks[packet.Type];
// Fire any registered callbacks
foreach (NetworkManager.PacketCallback callback in callbackArray)
{
if (callback != null)
{
callback(packet, this);
}
}
}
if (Callbacks.ContainsKey(PacketType.Default))
{
List callbackArray = Callbacks[PacketType.Default];
// Fire any registered callbacks
foreach (NetworkManager.PacketCallback callback in callbackArray)
{
if (callback != null)
{
callback(packet, this);
}
}
}
}
catch (Exception e)
{
Client.Log("Caught an exception in a packet callback: " + e.ToString(), Helpers.LogLevel.Warning);
}
#endregion FireCallbacks
}
private void AckTimer_Elapsed(object sender, ElapsedEventArgs ea)
{
if (connected)
{
SendAcks();
ResendUnacked();
}
}
}
///
/// 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
{
///
/// 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 SimDisconnectCallback(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 DisconnectCallback(DisconnectType reason, string message);
///
/// 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
}
///
/// The permanent UUID for the logged in avatar
///
public LLUUID AgentID;
///
/// A temporary UUID assigned to this session, used for secure
/// transactions
///
public LLUUID SessionID;
///
/// A string holding a descriptive error on login failure, empty
/// otherwise
///
public string LoginError;
///
/// The simulator that the logged in avatar is currently occupying
///
public Simulator CurrentSim;
///
/// 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; }
}
///
/// An event for the connection to a simulator other than the currently
/// occupied one disconnecting
///
public SimDisconnectCallback OnSimDisconnected;
///
/// An event for being logged out either through client request, server
/// forced, or network error
///
public DisconnectCallback OnDisconnected;
private SecondLife Client;
private Dictionary> Callbacks = new Dictionary>();
private List Simulators = new List();
private System.Timers.Timer DisconnectTimer;
private bool connected;
private const int NetworkTrafficTimeout = 15000;
private const int LoginTimeout = 60000;
///
///
///
///
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));
// Disconnect a sim if no network traffic has been received for 15 seconds
DisconnectTimer = new System.Timers.Timer(NetworkTrafficTimeout);
DisconnectTimer.Elapsed += new ElapsedEventHandler(DisconnectTimer_Elapsed);
}
///
///
///
///
///
public void RegisterCallback(PacketType type, PacketCallback callback)
{
if (!Callbacks.ContainsKey(type))
{
Callbacks[type] = new List();
}
List callbackArray = Callbacks[type];
callbackArray.Add(callback);
}
///
///
///
///
///
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);
}
}
///
///
///
///
public void SendPacket(Packet packet)
{
if (CurrentSim != null && CurrentSim.Connected)
{
CurrentSim.SendPacket(packet, true);
}
}
///
///
///
///
///
public void SendPacket(Packet packet, Simulator simulator)
{
if (simulator != null && simulator.Connected)
{
simulator.SendPacket(packet, true);
}
}
///
///
///
///
public void SendPacket(byte[] payload)
{
if (CurrentSim != null)
{
CurrentSim.SendPacket(payload);
}
else
{
throw new NotConnectedException();
}
}
///
/// Use this if you want to login to a specific location
///
///
///
///
///
/// string with a value that can be used in the start field in .DefaultLoginValues()
public static string StartLocation(string sim, int x, int y, int z)
{
//uri:sim&x&y&z
return "uri:" + sim.ToLower() + "&" + x + "&" + y + "&" + z;
}
///
///
///
///
///
///
///
///
///
public static Dictionary DefaultLoginValues(
string firstName, string lastName, string password, string userAgent, string author)
{
return DefaultLoginValues(firstName, lastName, password, "00:00:00:00:00:00", "last",
1, 50, 50, 50, "Win", "0", userAgent, author, false);
}
///
///
///
///
///
///
///
///
///
public static Dictionary DefaultLoginValues(
string firstName, string lastName, string password, string startLocation, string userAgent, string author,
bool md5pass)
{
return DefaultLoginValues(firstName, lastName, password, "00:00:00:00:00:00", startLocation,
1, 50, 50, 50, "Win", "0", userAgent, author, md5pass);
}
///
///
///
///
///
///
///
///
///
///
///
///
///
public static Dictionary DefaultLoginValues(string firstName,
string lastName, string password, string mac, string startLocation, string platform,
string viewerDigest, string userAgent, string author)
{
return DefaultLoginValues(firstName, lastName, password, mac, startLocation,
1, 50, 50, 50, platform, viewerDigest, userAgent, author, false);
}
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
public static 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 + " (" + Helpers.VERSION + ")";
values["author"] = author;
// Build the options array
List