using System; using System.Text; using System.Timers; using System.Collections; using System.Net; using System.Net.Sockets; using System.Threading; using System.Security.Cryptography; namespace libsecondlife { public delegate void PacketCallback(Packet packet, Circuit circuit); internal class AcceptAllCertificatePolicy : ICertificatePolicy { public AcceptAllCertificatePolicy() { } public bool CheckValidationResult(ServicePoint sPoint, System.Security.Cryptography.X509Certificates.X509Certificate cert, WebRequest wRequest,int certProb) { // Always accept return true; } } public class Circuit { public uint CircuitCode; public bool Opened; public ushort Sequence; public IPEndPoint ipEndPoint; private EndPoint endPoint; private ProtocolManager Protocol; private NetworkManager Network; private Hashtable UserCallbacks; private Hashtable InternalCallbacks; private byte[] Buffer; private Socket Connection; private AsyncCallback ReceivedData; private System.Timers.Timer OpenTimer; private System.Timers.Timer ACKTimer; private bool Timeout; private ArrayList AckOutbox; private Mutex AckOutboxMutex; private Hashtable NeedAck; private Mutex NeedAckMutex; private int ResendTick; public Circuit(ProtocolManager protocol, NetworkManager network, Hashtable userCallbacks, Hashtable internalCallbacks, uint circuitCode) { Protocol = protocol; Network = network; UserCallbacks = userCallbacks; InternalCallbacks = internalCallbacks; CircuitCode = circuitCode; Sequence = 0; Buffer = new byte[4096]; Connection = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); Opened = false; Timeout = false; // Initialize the queue of ACKs that need to be sent to the server AckOutbox = new ArrayList(); // Initialize the hashtable for reliable packets waiting on ACKs from the server NeedAck = new Hashtable(); // Create a timer to test if the connection times out OpenTimer = new System.Timers.Timer(10000); OpenTimer.Elapsed += new ElapsedEventHandler(OpenTimerEvent); // Create a timer to send PacketAcks and resend unACKed packets ACKTimer = new System.Timers.Timer(1000); ACKTimer.Elapsed += new ElapsedEventHandler(ACKTimerEvent); AckOutboxMutex = new Mutex(false, "AckOutboxMutex"); NeedAckMutex = new Mutex(false, "NeedAckMutex"); ResendTick = 0; } ~Circuit() { Stop(); Connection.Close(); } public bool Open(string ip, int port) { try { // Setup the callback ReceivedData = new AsyncCallback(this.OnReceivedData); // Create an endpoint that we will be communicating with (need it in two types due to // .NET weirdness) ipEndPoint = new IPEndPoint(IPAddress.Parse(ip), port); endPoint = (EndPoint)ipEndPoint; // Associate this circuit's socket with the given ip and port and start listening Connection.Connect(endPoint); Connection.BeginReceiveFrom(Buffer, 0, Buffer.Length, SocketFlags.None, ref endPoint, ReceivedData, null); // Start the circuit opening timeout OpenTimer.Start(); // Start the packet resend timer ACKTimer.Start(); // Send the UseCircuitCode packet to initiate the connection Packet packet = PacketBuilder.UseCircuitCode(Protocol, Network.LoginValues.AgentID, Network.LoginValues.SessionID, CircuitCode); // Send the initial packet out SendPacket(packet, true); while (!Timeout) { if (Opened) { return true; } Thread.Sleep(0); } } catch (Exception e) { Helpers.Log(e.ToString(), Helpers.LogLevel.Error); } return false; } public void Close() { try { Stop(); // Send the CloseCircuit notice Packet packet = new Packet("CloseCircuit", Protocol, 8); SendPacket(packet, true); // Send any last ACKs before closing the circuit SendACKs(); Connection.Close(); } catch (Exception e) { Helpers.Log(e.ToString(), Helpers.LogLevel.Error); } } public void Stop() { try { // Stop the resend timer ACKTimer.Stop(); // Stop the open circuit timer (just in case it's still running) OpenTimer.Stop(); // TODO: Is this safe? Using the mutex throws an exception about a disposed object NeedAck.Clear(); } catch (Exception e) { Helpers.Log(e.ToString(), Helpers.LogLevel.Error); } } public void SendPacket(Packet packet, bool incrementSequence) { byte[] zeroBuffer = new byte[4096]; int zeroBytes; // DEBUG //Console.WriteLine("Sending " + packet.Data.Length + " byte " + packet.Layout.Name); try { if ((packet.Data[0] & Helpers.MSG_RELIABLE) != 0 && incrementSequence) { if (!NeedAck.ContainsKey(packet)) { // This packet needs an ACK, keep track of when it was sent out NeedAckMutex.WaitOne(); NeedAck.Add(packet, Environment.TickCount); NeedAckMutex.ReleaseMutex(); } } if (incrementSequence) { // Set the sequence number here since we are manually serializing the packet packet.Sequence = ++Sequence; } // Zerocode if needed if ((packet.Data[0] & Helpers.MSG_ZEROCODED) != 0) { zeroBytes = Helpers.ZeroEncode(packet.Data, packet.Data.Length, zeroBuffer); } else { // Normal packet, copy it straight over to the zeroBuffer Array.Copy(packet.Data, 0, zeroBuffer, 0, packet.Data.Length); zeroBytes = packet.Data.Length; } // The incrementSequence check prevents a possible deadlock situation if (AckOutbox.Count != 0 && incrementSequence && packet.Layout.Name != "PacketAck" && packet.Layout.Name != "LogoutRequest") { // Claim the mutex on the AckOutbox AckOutboxMutex.WaitOne(); //TODO: Make sure we aren't appending more than 255 ACKs // Append each ACK needing to be sent out to this packet foreach (uint ack in AckOutbox) { Array.Copy(BitConverter.GetBytes(ack), 0, zeroBuffer, zeroBytes - 1, 4); zeroBytes += 4; } // Last byte is the number of ACKs zeroBuffer[zeroBytes - 1] = (byte)AckOutbox.Count; zeroBytes += 1; AckOutbox.Clear(); // Release the mutex AckOutboxMutex.ReleaseMutex(); // Set the flag that this packet has ACKs appended to it zeroBuffer[0] += Helpers.MSG_APPENDED_ACKS; } int numSent = Connection.Send(zeroBuffer, zeroBytes, SocketFlags.None); // DEBUG //Console.WriteLine("Sent " + numSent + " bytes"); } catch (Exception e) { Helpers.Log(e.ToString(), Helpers.LogLevel.Error); } } private void SendACKs() { // Claim the mutex on the AckOutbox AckOutboxMutex.WaitOne(); if (AckOutbox.Count != 0) { try { Packet packet = PacketBuilder.PacketAck(Protocol, AckOutbox); if (packet.Data.Length < 13) { Helpers.Log("Trying to send a PacketAck with no ACKs, cancelling", Helpers.LogLevel.Warning); // Release the mutex AckOutboxMutex.ReleaseMutex(); return; } // Set the sequence number packet.Sequence = ++Sequence; // Bypass SendPacket since we are taking care of the AckOutbox ourself int numSent = Connection.Send(packet.Data); // DEBUG //Console.WriteLine("Sent " + numSent + " byte " + packet.Layout.Name); AckOutbox.Clear(); } catch (Exception e) { Helpers.Log(e.ToString(), Helpers.LogLevel.Error); } } // Release the mutex AckOutboxMutex.ReleaseMutex(); } private void OnReceivedData(IAsyncResult result) { Packet packet; try { // For the UseCircuitCode timeout Opened = true; OpenTimer.Stop(); // Retrieve the incoming packet int numBytes = Connection.EndReceiveFrom(result, ref endPoint); if ((Buffer[Buffer.Length - 1] & Helpers.MSG_APPENDED_ACKS) != 0) { // Grab the ACKs that are appended to this packet byte numAcks = Buffer[Buffer.Length - 1]; Helpers.Log("Found " + numAcks + " appended acks", Helpers.LogLevel.Info); // Claim the NeedAck mutex NeedAckMutex.WaitOne(); for (int i = 1; i <= numAcks; ++i) { uint ack = BitConverter.ToUInt32(Buffer, numBytes - i * 4 - 1); Beginning: ICollection reliablePackets = NeedAck.Keys; // Remove this packet if it exists foreach (Packet reliablePacket in reliablePackets) { if ((uint)reliablePacket.Sequence == ack) { NeedAck.Remove(reliablePacket); goto Beginning; } } } // Release the mutex NeedAckMutex.ReleaseMutex(); // Adjust the packet length numBytes = numBytes - numAcks * 4 - 1; } if ((Buffer[0] & Helpers.MSG_ZEROCODED) != 0) { // Allocate a temporary buffer for the zerodecoded packet byte[] zeroBuffer = new byte[4096]; int zeroBytes = Helpers.ZeroDecode(Buffer, numBytes, zeroBuffer); packet = new Packet(zeroBuffer, zeroBytes, Protocol); numBytes = zeroBytes; } else { // Create the packet object from our byte array packet = new Packet(Buffer, numBytes, Protocol); } // DEBUG //Console.WriteLine("Received a " + numBytes + " byte " + packet.Layout.Name); // Start listening again since we're done with Buffer Connection.BeginReceiveFrom(Buffer, 0, Buffer.Length, SocketFlags.None, ref endPoint, ReceivedData, null); if ((packet.Data[0] & Helpers.MSG_RELIABLE) != 0) { if (!AckOutbox.Contains((uint)packet.Sequence)) { // This packet needs to be ACKed, push its sequence number on to the queue AckOutboxMutex.WaitOne(); AckOutbox.Add((uint)packet.Sequence); AckOutboxMutex.ReleaseMutex(); } else { if ((packet.Data[0] & Helpers.MSG_RESENT) != 0) { // We received a resent packet Helpers.Log("Received a resent packet, sequence=" + packet.Sequence, Helpers.LogLevel.Warning); return; } else { // We received a resent packet Helpers.Log("Received a duplicate sequence number? sequence=" + packet.Sequence + ", name=" + packet.Layout.Name, Helpers.LogLevel.Warning); } } } if (packet.Layout.Name == null) { Helpers.Log("Received an unrecognized packet", Helpers.LogLevel.Warning); return; } else if (packet.Layout.Name == "PacketAck") { // PacketAck is handled directly instead of using a callback to simplify access to // the NeedAck hashtable and its mutex ArrayList blocks = packet.Blocks(); NeedAckMutex.WaitOne(); // Remove each ACK in this packet from the NeedAck waiting list foreach (Block block in blocks) { foreach (Field field in block.Fields) { Beginning: ICollection reliablePackets = NeedAck.Keys; // Remove this packet if it exists foreach (Packet reliablePacket in reliablePackets) { if ((uint)reliablePacket.Sequence == (uint)field.Data) { NeedAck.Remove(reliablePacket); // Restart the loop to avoid upsetting the enumerator goto Beginning; } } } } NeedAckMutex.ReleaseMutex(); } // Fire any internal callbacks registered with this packet type PacketCallback callback = (PacketCallback)InternalCallbacks[packet.Layout.Name]; if (callback != null) { callback(packet, this); } // Fire any user callbacks registered with this packet type callback = (PacketCallback)UserCallbacks[packet.Layout.Name]; if (callback != null) { callback(packet, this); } else { // Attempt to fire a default user callback callback = (PacketCallback)UserCallbacks["Default"]; if (callback != null) { callback(packet, this); } } } catch (Exception e) { Helpers.Log(e.ToString(), Helpers.LogLevel.Error); } } private void OpenTimerEvent(object source, System.Timers.ElapsedEventArgs ea) { try { Timeout = true; OpenTimer.Stop(); } catch (Exception e) { Helpers.Log(e.ToString(), Helpers.LogLevel.Error); } } private void ACKTimerEvent(object source, System.Timers.ElapsedEventArgs ea) { try { // Send any ACKs in the queue SendACKs(); ResendTick++; if (ResendTick >= 3) { ResendTick = 0; // Claim the NeedAck mutex NeedAckMutex.WaitOne(); Beginning: // Check if any reliable packets haven't been ACKed by the server IDictionaryEnumerator packetEnum = NeedAck.GetEnumerator(); while (packetEnum.MoveNext()) { int ticks = (int)packetEnum.Value; // TODO: Is this hardcoded value correct? Should it be a higher level define or a // changeable property? if (Environment.TickCount - ticks > 3000) { Packet packet = (Packet)packetEnum.Key; // Adjust the timeout value for this packet NeedAck[packet] = Environment.TickCount; // Add the resent flag packet.Data[0] += Helpers.MSG_RESENT; // Resend the packet SendPacket((Packet)packet, false); // Restart the loop since we modified a value and the iterator will fail goto Beginning; } } // Release the mutex NeedAckMutex.ReleaseMutex(); } } catch (Exception e) { Helpers.Log(e.ToString(), Helpers.LogLevel.Error); } } } public struct LoginReply { public LLUUID SessionID; public LLUUID SecureSessionID; public string StartLocation; public string FirstName; public string LastName; public int RegionX; public int RegionY; public string Home; public string Message; public uint CircuitCode; public int Port; public string IP; public string LookAt; public LLUUID AgentID; public uint SecondsSinceEpoch; } public class NetworkManager { public LoginReply LoginValues; public string LoginError; public Hashtable UserCallbacks; public Circuit CurrentCircuit; private ProtocolManager Protocol; private string LoginBuffer; private ArrayList Circuits; private Hashtable InternalCallbacks; public NetworkManager(ProtocolManager protocol) { Protocol = protocol; Circuits = new ArrayList(); UserCallbacks = new Hashtable(); InternalCallbacks = new Hashtable(); CurrentCircuit = null; // Register the internal callbacks PacketCallback callback = new PacketCallback(RegionHandshakeHandler); InternalCallbacks["RegionHandshake"] = callback; callback = new PacketCallback(StartPingCheckHandler); InternalCallbacks["StartPingCheck"] = callback; } public void SendPacket(Packet packet) { if (CurrentCircuit != null) { CurrentCircuit.SendPacket(packet, true); } else { Helpers.Log("Trying to send a packet when there is no current circuit", Helpers.LogLevel.Error); } } public void SendPacket(Packet packet, Circuit circuit) { circuit.SendPacket(packet, true); } public bool Login(string firstName, string lastName, string password, string mac, int major, int minor, int patch, int build, string platform, string viewerDigest, string userAgent, string author) { return Login(firstName, lastName, password, mac, major, minor, patch, build, platform, viewerDigest, userAgent, author, "https://login.agni.lindenlab.com/cgi-bin/login.cgi"); } public bool Login(string firstName, string lastName, string password, string mac, int major, int minor, int patch, int build, string platform, string viewerDigest, string userAgent, string author, string url) { WebRequest login; WebResponse response; // Generate an MD5 hash of the password MD5 md5 = new MD5CryptoServiceProvider(); byte[] hash = md5.ComputeHash(Encoding.ASCII.GetBytes(password)); StringBuilder passwordDigest = new StringBuilder(); // Convert the hash to a hex string foreach(byte b in hash) { passwordDigest.AppendFormat("{0:x2}", b); } string loginRequest = "login_to_simulator" + "" + "first" + firstName + "" + "last" + lastName + "" + "passwd$1$" + passwordDigest + "" + "startlast" + "major" + major + "" + "minor" + minor + "" + "patch" + patch + "" + "build" + build + "" + "platform" + platform + "" + "mac" + mac + "" + "viewer_digest" + viewerDigest + "" + "user-agent" + userAgent + " (" + Helpers.VERSION + ")" + "author" + author + "" + "" ; // Override SSL authentication mechanisms ServicePointManager.CertificatePolicy = new AcceptAllCertificatePolicy(); login = WebRequest.Create(url); login.ContentType = "text/xml"; login.Method = "POST"; login.Timeout = 12000; byte[] request = System.Text.Encoding.ASCII.GetBytes(loginRequest); login.ContentLength = request.Length; System.IO.Stream stream = login.GetRequestStream(); try { stream.Write(request, 0, request.Length); stream.Close(); response = login.GetResponse(); if (response == null) { LoginError = "Error logging in: (Unknown)"; Helpers.Log(LoginError, Helpers.LogLevel.Warning); return false; } //TODO: To support UTF8 avatar names the encoding should be handled better System.IO.StreamReader streamReader = new System.IO.StreamReader(response.GetResponseStream(), System.Text.Encoding.ASCII); LoginBuffer = streamReader.ReadToEnd(); streamReader.Close(); response.Close(); } catch (Exception e) { LoginError = "Caught an exception logging in: " + e.ToString(); Helpers.Log(LoginError, Helpers.LogLevel.Warning); } // Parse the login reply and put the returned variables in to a struct if (!ParseLoginReply()) { return false; } // Connect to the sim given in the login reply Circuit circuit = new Circuit(Protocol, this, UserCallbacks, InternalCallbacks, LoginValues.CircuitCode); if (!circuit.Open(LoginValues.IP, LoginValues.Port)) { return false; } // Circuit was successfully opened, add it to the list and set it as default Circuits.Add(circuit); CurrentCircuit = circuit; // Move our agent in to the sim to complete the connection Packet packet = PacketBuilder.CompleteAgentMovement(Protocol, LoginValues.AgentID, LoginValues.SessionID, LoginValues.CircuitCode); SendPacket(packet); return true; } public void Logout() { // TODO: Close all circuits except the current one // Halt all timers on the current circuit CurrentCircuit.Stop(); Packet packet = PacketBuilder.LogoutRequest(Protocol, LoginValues.AgentID, LoginValues.SessionID); SendPacket(packet); // TODO: We should probably check if the server actually received the logout request // Instead we'll use this silly Sleep() System.Threading.Thread.Sleep(1000); } private bool ParseLoginReply() { string msg; msg = RpcGetString(LoginBuffer, "reason"); if (msg.Length != 0) { LoginError = RpcGetString(LoginBuffer, "message"); return false; } msg = RpcGetString(LoginBuffer, "logintrue"); if (msg.Length == 0) { LoginError = "Unknown login error"; return false; } // Grab the login parameters LoginValues.SessionID = RpcGetString(LoginBuffer.ToString(), "session_id"); LoginValues.SecureSessionID = RpcGetString(LoginBuffer.ToString(), "secure_session_id"); LoginValues.StartLocation = RpcGetString(LoginBuffer.ToString(), "start_location"); LoginValues.FirstName = RpcGetString(LoginBuffer.ToString(), "first_name"); LoginValues.LastName = RpcGetString(LoginBuffer.ToString(), "last_name"); LoginValues.RegionX = RpcGetInt(LoginBuffer.ToString(), "region_x"); LoginValues.RegionY = RpcGetInt(LoginBuffer.ToString(), "region_y"); LoginValues.Home = RpcGetString(LoginBuffer.ToString(), "home"); LoginValues.Message = RpcGetString(LoginBuffer.ToString(), "message").Replace("\r\n", ""); LoginValues.CircuitCode = (uint)RpcGetInt(LoginBuffer.ToString(), "circuit_code"); LoginValues.Port = RpcGetInt(LoginBuffer.ToString(), "sim_port"); LoginValues.IP = RpcGetString(LoginBuffer.ToString(), "sim_ip"); LoginValues.LookAt = RpcGetString(LoginBuffer.ToString(), "look_at"); LoginValues.AgentID = RpcGetString(LoginBuffer.ToString(), "agent_id"); LoginValues.SecondsSinceEpoch = (uint)RpcGetInt(LoginBuffer.ToString(), "seconds_since_epoch"); return true; } string RpcGetString(string rpc, string name) { int pos = rpc.IndexOf(name); int pos2; if (pos == -1) { return ""; } rpc = rpc.Substring(pos, rpc.Length - pos); pos = rpc.IndexOf(""); if (pos == -1) { return ""; } rpc = rpc.Substring(pos + 8, rpc.Length - (pos + 8)); pos2 = rpc.IndexOf(""); if (pos2 == -1) { return ""; } return rpc.Substring(0, pos2); } int RpcGetInt(string rpc, string name) { int pos = rpc.IndexOf(name); int pos2; if (pos == -1) { return -1; } rpc = rpc.Substring(pos, rpc.Length - pos); pos = rpc.IndexOf(""); if (pos == -1) { return -1; } rpc = rpc.Substring(pos + 4, rpc.Length - (pos + 4)); pos2 = rpc.IndexOf(""); if (pos2 == -1) { return -1; } return Int32.Parse(rpc.Substring(0, pos2)); } private void StartPingCheckHandler(Packet packet, Circuit circuit) { //TODO: Should we care about OldestUnacked? // Respond to the ping request Packet pingPacket = PacketBuilder.CompletePingCheck(Protocol, packet.Data[5]); SendPacket(pingPacket, circuit); } private void RegionHandshakeHandler(Packet packet, Circuit circuit) { // Send a RegionHandshakeReply Packet replyPacket = new Packet("RegionHandshakeReply", Protocol, 12); SendPacket(replyPacket, circuit); } } }