diff --git a/OpenMetaverse/Capabilities/EventQueueListener.cs b/OpenMetaverse/Capabilities/EventQueueListener.cs deleted file mode 100644 index ce1c0e6d..00000000 --- a/OpenMetaverse/Capabilities/EventQueueListener.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2007-2008, openmetaverse.org - * 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 openmetaverse.org 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.Net; - -#if !PocketPC - -namespace OpenMetaverse.Capabilities -{ - public class EventQueueListener - { - } -} - -#endif diff --git a/OpenMetaverse/Capabilities/CapsListener.cs b/OpenMetaverse/Capabilities/EventQueueServer.cs similarity index 92% rename from OpenMetaverse/Capabilities/CapsListener.cs rename to OpenMetaverse/Capabilities/EventQueueServer.cs index 8bc0199f..ea1cfccc 100644 --- a/OpenMetaverse/Capabilities/CapsListener.cs +++ b/OpenMetaverse/Capabilities/EventQueueServer.cs @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007-2008, openmetaverse.org + * Copyright (c) 2008, openmetaverse.org * All rights reserved. * * - Redistribution and use in source and binary forms, with or without @@ -31,7 +31,7 @@ using System.Net; namespace OpenMetaverse.Capabilities { - public class CapsListener + public class EventQueueServer { } } diff --git a/OpenMetaverse/Capabilities/HttpRequestSignature.cs b/OpenMetaverse/Capabilities/HttpRequestSignature.cs new file mode 100644 index 00000000..3214a60a --- /dev/null +++ b/OpenMetaverse/Capabilities/HttpRequestSignature.cs @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2008, openmetaverse.org + * 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 openmetaverse.org 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.Net; +using System.Text.RegularExpressions; + +namespace OpenMetaverse.Capabilities +{ + /// + /// Used to match incoming HTTP requests against registered handlers. + /// Matches based on any combination of HTTP Method, Content-Type header, + /// and URL path. URL path matching supports the * wildcard character + /// + public struct HttpRequestSignature : IEquatable + { + private string method; + private string contentType; + private string path; + private bool pathIsWildcard; + + public string Method + { + get { return method; } + set + { + if (!String.IsNullOrEmpty(value)) method = value.ToLower(); + else method = String.Empty; + } + } + public string ContentType + { + get { return contentType; } + set + { + if (!String.IsNullOrEmpty(value)) contentType = value.ToLower(); + else contentType = String.Empty; + } + } + public string Path + { + get { return path; } + set + { + pathIsWildcard = false; + + if (!String.IsNullOrEmpty(value)) + { + // Regex to tear apart URLs, used to extract just a URL + // path from any data we're given + string regexPattern = + @"^(?(?[^:/\?#]+):)?(?" + + @"//(?[^/\?#]*))?(?[^\?#]*)" + + @"(?\?(?[^#]*))?" + + @"(?#(?.*))?"; + Regex re = new Regex(regexPattern, RegexOptions.ExplicitCapture); + Match m = re.Match(value); + string newPath = m.Groups["p0"].Value.ToLower(); + + // Remove any trailing forward-slashes + if (newPath.EndsWith("/")) + newPath = newPath.Substring(0, newPath.Length - 1); + + // Check if this path contains a wildcard. If so, convert it to a regular expression + if (newPath.Contains("*")) + { + pathIsWildcard = true; + newPath = String.Format("^{0}$", newPath.Replace("\\*", ".*")); + } + + path = newPath; + } + else + { + path = String.Empty; + } + } + } + public bool PathIsWildcard + { + get { return pathIsWildcard; } + } + + public HttpRequestSignature(HttpListenerContext context) + { + method = contentType = path = String.Empty; + pathIsWildcard = false; + + Method = context.Request.HttpMethod; + ContentType = context.Request.ContentType; + Path = context.Request.RawUrl; + } + + public bool ExactlyEquals(HttpRequestSignature signature) + { + return (method.Equals(signature.Method) && contentType.Equals(signature.ContentType) && path.Equals(signature.Path)); + } + + public override bool Equals(object obj) + { + return (obj is HttpRequestSignature) ? this == (HttpRequestSignature)obj : false; + } + + public bool Equals(HttpRequestSignature signature) + { + return (this == signature); + } + + public override int GetHashCode() + { + int hash = (method != null) ? method.GetHashCode() : 0; + hash ^= (contentType != null) ? contentType.GetHashCode() : 0; + hash ^= (path != null) ? path.GetHashCode() : 0; + return hash; + } + + public static bool operator ==(HttpRequestSignature lhs, HttpRequestSignature rhs) + { + bool methodMatch = (String.IsNullOrEmpty(lhs.Method) || String.IsNullOrEmpty(rhs.Method) || lhs.Method.Equals(rhs.Method)); + bool contentTypeMatch = (String.IsNullOrEmpty(lhs.ContentType) || String.IsNullOrEmpty(rhs.ContentType) || lhs.ContentType.Equals(rhs.ContentType)); + bool pathMatch = false; + + if (methodMatch && contentTypeMatch) + { + if (!String.IsNullOrEmpty(lhs.Path) && !String.IsNullOrEmpty(rhs.Path)) + { + // Do wildcard matching if there is any to be done + if (lhs.PathIsWildcard) + pathMatch = Regex.IsMatch(rhs.Path, lhs.Path); + else if (rhs.PathIsWildcard) + pathMatch = Regex.IsMatch(lhs.Path, rhs.Path); + else + pathMatch = lhs.Path.Equals(rhs.Path); + } + else + { + pathMatch = true; + } + } + + return (methodMatch && contentTypeMatch && pathMatch); + } + + public static bool operator !=(HttpRequestSignature lhs, HttpRequestSignature rhs) + { + return !(lhs == rhs); + } + } +} diff --git a/OpenMetaverse/Capabilities/HttpServer.cs b/OpenMetaverse/Capabilities/HttpServer.cs new file mode 100644 index 00000000..221ae406 --- /dev/null +++ b/OpenMetaverse/Capabilities/HttpServer.cs @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2008, openmetaverse.org + * 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 openmetaverse.org 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.Collections.Generic; +using System.Net; + +namespace OpenMetaverse.Capabilities +{ + public class HttpServer + { + public delegate void HttpRequestCallback(ref HttpListenerContext context); + + public struct HttpRequestHandler : IEquatable + { + public HttpRequestSignature Signature; + public HttpRequestCallback Callback; + + public HttpRequestHandler(HttpRequestSignature signature, HttpRequestCallback callback) + { + Signature = signature; + Callback = callback; + } + + public override bool Equals(object obj) + { + return (obj is HttpRequestHandler) ? this.Signature == ((HttpRequestHandler)obj).Signature : false; + } + + public override int GetHashCode() + { + return Signature.GetHashCode(); + } + + public bool Equals(HttpRequestHandler handler) + { + return this.Signature == handler.Signature; + } + } + + HttpListener server; + AsyncCallback serverCallback; + int serverPort; + bool sslEnabled; + List requestHandlers; + + bool isRunning; + + public HttpServer(int port, bool ssl) + { + serverPort = port; + sslEnabled = ssl; + server = new HttpListener(); + serverCallback = new AsyncCallback(BeginGetContextCallback); + + if (ssl) + server.Prefixes.Add(String.Format("https://+:{0}/", port)); + else + server.Prefixes.Add(String.Format("http://+:{0}/", port)); + + requestHandlers = new List(); + + isRunning = false; + } + + public void AddHandler(HttpRequestHandler handler) + { + if (!isRunning) + requestHandlers.Add(handler); + else + throw new InvalidOperationException("Cannot add HTTP request handlers while the server is running"); + } + + public void RemoveHandler(HttpRequestHandler handler) + { + if (!isRunning) + requestHandlers.Remove(handler); + else + throw new InvalidOperationException("Cannot add HTTP request handlers while the server is running"); + } + + public void Start() + { + server.Start(); + server.BeginGetContext(serverCallback, server); + isRunning = true; + } + + public void Stop() + { + isRunning = false; + try { server.Stop(); } + catch (ObjectDisposedException) { } + } + + protected void BeginGetContextCallback(IAsyncResult result) + { + HttpListenerContext context = null; + + // Retrieve the incoming request + try { context = server.EndGetContext(result); } + catch (Exception) + { + } + + if (isRunning) + { + // Immediately start listening again + try { server.BeginGetContext(serverCallback, server); } + catch (Exception) + { + // Something went wrong, can't resume listening. Bail out now + // since this is a shutdown (whether it was meant to be or not) + return; + } + + // Process the incoming request + if (context != null) + { + // Create a request signature + HttpRequestSignature signature = new HttpRequestSignature(context); + + // Look for a signature match in our handlers + for (int i = 0; i < requestHandlers.Count; i++) + { + HttpRequestHandler handler = requestHandlers[i]; + + if (handler.Signature == signature) + { + // Request signature matched, handle it + try + { + handler.Callback(ref context); + } + catch (Exception e) + { + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + context.Response.StatusDescription = e.ToString(); + } + + context.Response.Close(); + return; + } + } + + // No registered handler matched this request's signature. Send a 404 + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + context.Response.StatusDescription = String.Format( + "No request handler registered for Method=\"{0}\", Content-Type=\"{1}\", Path=\"{2}\"", + signature.Method, signature.ContentType, signature.Path); + context.Response.Close(); + } + } + } + } +} diff --git a/OpenMetaverse/EventDictionary.cs b/OpenMetaverse/EventDictionary.cs index 0288003f..c724fbc6 100644 --- a/OpenMetaverse/EventDictionary.cs +++ b/OpenMetaverse/EventDictionary.cs @@ -110,18 +110,28 @@ namespace OpenMetaverse { NetworkManager.PacketCallback callback; - if (_EventTable.TryGetValue(packetType, out callback)) + // Default handler first, if one exists + if (_EventTable.TryGetValue(PacketType.Default, out callback)) { - try - { - callback(packet, simulator); - } + try { callback(packet, simulator); } catch (Exception ex) { - Logger.Log("Packet Event Handler: " + ex.ToString(), Helpers.LogLevel.Error, Client); + Logger.Log("Default packet event handler: " + ex.ToString(), Helpers.LogLevel.Error, Client); } } - else if (packetType != PacketType.Default && packetType != PacketType.PacketAck) + + if (_EventTable.TryGetValue(packetType, out callback)) + { + try { callback(packet, simulator); } + catch (Exception ex) + { + Logger.Log("Packet event handler: " + ex.ToString(), Helpers.LogLevel.Error, Client); + } + + return; + } + + if (packetType != PacketType.Default && packetType != PacketType.PacketAck) { Logger.DebugLog("No handler registered for packet event " + packetType, Client); } @@ -136,12 +146,24 @@ namespace OpenMetaverse internal void BeginRaiseEvent(PacketType packetType, Packet packet, Simulator simulator) { NetworkManager.PacketCallback callback; + PacketCallbackWrapper wrapper; + + // Default handler first, if one exists + if (_EventTable.TryGetValue(PacketType.Default, out callback)) + { + if (callback != null) + { + wrapper.Callback = callback; + wrapper.Packet = packet; + wrapper.Simulator = simulator; + ThreadPool.QueueUserWorkItem(_ThreadPoolCallback, wrapper); + } + } if (_EventTable.TryGetValue(packetType, out callback)) { if (callback != null) { - PacketCallbackWrapper wrapper; wrapper.Callback = callback; wrapper.Packet = packet; wrapper.Simulator = simulator; diff --git a/OpenMetaverse/Login.cs b/OpenMetaverse/Login.cs index 297aaaa4..51b0739c 100644 --- a/OpenMetaverse/Login.cs +++ b/OpenMetaverse/Login.cs @@ -29,6 +29,7 @@ using System.Collections.Generic; using System.Threading; using System.IO; using System.Net; +using System.Xml; using System.Security.Cryptography.X509Certificates; using OpenMetaverse.StructuredData; using OpenMetaverse.Capabilities; @@ -100,6 +101,9 @@ namespace OpenMetaverse public struct LoginResponseData { + public bool Success; + public string Reason; + public string Message; public UUID AgentID; public UUID SessionID; public UUID SecureSessionID; @@ -140,9 +144,7 @@ namespace OpenMetaverse } catch (LLSDException e) { - // FIXME: sometimes look_at comes back with invalid values e.g: 'look_at':'[r1,r2.0193899999999998204e-06,r0]' - // need to handle that somehow - Logger.DebugLog("login server returned (some) invalid data: " + e.Message); + Logger.DebugLog("Login server returned (some) invalid data: " + e.Message); } // Home @@ -209,6 +211,167 @@ namespace OpenMetaverse LibraryFolders = ParseInventoryFolders("inventory-skel-lib", LibraryOwner, reply); } + public void ToXmlRpc(XmlWriter writer) + { + writer.WriteStartElement("methodResponse"); + { + writer.WriteStartElement("params"); + writer.WriteStartElement("param"); + writer.WriteStartElement("value"); + writer.WriteStartElement("struct"); + { + if (Success) + { + // session_id + WriteXmlRpcStringMember(writer, false, "session_id", SessionID.ToString()); + + // ui-config + WriteXmlRpcArrayStart(writer, "ui-config"); + WriteXmlRpcStringMember(writer, true, "allow_first_life", "Y"); + WriteXmlRpcArrayEnd(writer); + + // inventory-lib-owner + WriteXmlRpcArrayStart(writer, "inventory-lib-owner"); + WriteXmlRpcStringMember(writer, true, "agent_id", LibraryOwner.ToString()); + WriteXmlRpcArrayEnd(writer); + + // start_location + WriteXmlRpcStringMember(writer, false, "start_location", StartLocation); + + // seconds_since_epoch + WriteXmlRpcIntMember(writer, false, "seconds_since_epoch", Utils.DateTimeToUnixTime(SecondsSinceEpoch)); + + // event_categories + WriteXmlRpcArrayStart(writer, "event_categories"); + writer.WriteStartElement("struct"); writer.WriteEndElement(); + WriteXmlRpcArrayEnd(writer); + + // classified_categories + WriteXmlRpcArrayStart(writer, "classified_categories"); + writer.WriteStartElement("struct"); writer.WriteEndElement(); + WriteXmlRpcArrayEnd(writer); + + // inventory-root + WriteXmlRpcArrayStart(writer, "inventory-root"); + WriteXmlRpcStringMember(writer, true, "folder_id", InventoryRoot.ToString()); + WriteXmlRpcArrayEnd(writer); + + // sim_port + WriteXmlRpcIntMember(writer, false, "sim_port", SimPort); + + // agent_id + WriteXmlRpcStringMember(writer, false, "agent_id", AgentID.ToString()); + + // agent_access + WriteXmlRpcStringMember(writer, false, "agent_access", AgentAccess); + + // inventory-skeleton + WriteXmlRpcArrayStart(writer, "inventory-skeleton"); + writer.WriteStartElement("struct"); writer.WriteEndElement(); + WriteXmlRpcArrayEnd(writer); + + // buddy-list + WriteXmlRpcArrayStart(writer, "buddy-list"); + WriteXmlRpcArrayEnd(writer); + + // first_name + WriteXmlRpcStringMember(writer, false, "first_name", FirstName); + + // global-textures + WriteXmlRpcArrayStart(writer, "global-textures"); + writer.WriteStartElement("struct"); writer.WriteEndElement(); + WriteXmlRpcArrayEnd(writer); + + // inventory-skel-lib + WriteXmlRpcArrayStart(writer, "inventory-skel-lib"); + writer.WriteStartElement("struct"); writer.WriteEndElement(); + WriteXmlRpcArrayEnd(writer); + + // seed_capability + WriteXmlRpcStringMember(writer, false, "seed_capability", SeedCapability); + + // gestures + WriteXmlRpcArrayStart(writer, "gestures"); + writer.WriteStartElement("struct"); writer.WriteEndElement(); + WriteXmlRpcArrayEnd(writer); + + // sim_ip + WriteXmlRpcStringMember(writer, false, "sim_ip", SimIP.ToString()); + + // inventory-lib-root + WriteXmlRpcArrayStart(writer, "inventory-lib-root"); + WriteXmlRpcStringMember(writer, true, "folder_id", LibraryRoot.ToString()); + WriteXmlRpcArrayEnd(writer); + + // login-flags + WriteXmlRpcArrayStart(writer, "login-flags"); + writer.WriteStartElement("struct"); writer.WriteEndElement(); + WriteXmlRpcArrayEnd(writer); + + // inventory_host + WriteXmlRpcStringMember(writer, false, "inventory_host", String.Empty); + + // home + LLSDMap home = new LLSDMap(3); + + LLSDArray homeRegionHandle = new LLSDArray(2); + uint homeRegionX, homeRegionY; + Helpers.LongToUInts(HomeRegion, out homeRegionX, out homeRegionY); + homeRegionHandle.Add(LLSD.FromReal((double)homeRegionX)); + homeRegionHandle.Add(LLSD.FromReal((double)homeRegionY)); + + home["region_handle"] = homeRegionHandle; + home["position"] = LLSD.FromVector3(HomePosition); + home["look_at"] = LLSD.FromVector3(HomeLookAt); + + WriteXmlRpcStringMember(writer, false, "home", LLSDParser.SerializeNotation(home)); + + // message + WriteXmlRpcStringMember(writer, false, "message", Message); + + // look_at + WriteXmlRpcStringMember(writer, false, "look_at", LLSDParser.SerializeNotation(LLSD.FromVector3(LookAt))); + + // login + WriteXmlRpcStringMember(writer, false, "login", "true"); + + // event_notifications + WriteXmlRpcArrayStart(writer, "event_notifications"); + WriteXmlRpcArrayEnd(writer); + + // secure_session_id + WriteXmlRpcStringMember(writer, false, "secure_session_id", SecureSessionID.ToString()); + + // region_x + WriteXmlRpcIntMember(writer, false, "region_x", RegionX); + + // last_name + WriteXmlRpcStringMember(writer, false, "last_name", LastName); + + // region_y + WriteXmlRpcIntMember(writer, false, "region_y", RegionY); + + // circuit_code + WriteXmlRpcIntMember(writer, false, "circuit_code", CircuitCode); + + // initial-outfit + WriteXmlRpcArrayStart(writer, "initial-outfit"); + WriteXmlRpcArrayEnd(writer); + } + else + { + // Login failure + } + } + writer.WriteEndElement(); + writer.WriteEndElement(); + writer.WriteEndElement(); + writer.WriteEndElement(); + } + writer.WriteEndElement(); + writer.Close(); + } + #region Parsing Helpers public static uint ParseUInt(string key, LLSDMap reply) @@ -305,6 +468,75 @@ namespace OpenMetaverse } #endregion Parsing Helpers + + #region XmlRpc Serializing Helpers + + public static void WriteXmlRpcStringMember(XmlWriter writer, bool wrapWithValueStruct, string name, string value) + { + if (wrapWithValueStruct) + { + writer.WriteStartElement("value"); + writer.WriteStartElement("struct"); + } + writer.WriteStartElement("member"); + { + writer.WriteElementString("name", name); + writer.WriteStartElement("value"); + { + writer.WriteElementString("string", value); + } + writer.WriteEndElement(); + } + writer.WriteEndElement(); + if (wrapWithValueStruct) + { + writer.WriteEndElement(); + writer.WriteEndElement(); + } + } + + public static void WriteXmlRpcIntMember(XmlWriter writer, bool wrapWithValueStruct, string name, uint value) + { + if (wrapWithValueStruct) + { + writer.WriteStartElement("value"); + writer.WriteStartElement("struct"); + } + writer.WriteStartElement("member"); + { + writer.WriteElementString("name", name); + writer.WriteStartElement("value"); + { + writer.WriteElementString("i4", value.ToString()); + } + writer.WriteEndElement(); + } + writer.WriteEndElement(); + if (wrapWithValueStruct) + { + writer.WriteEndElement(); + writer.WriteEndElement(); + } + } + + public static void WriteXmlRpcArrayStart(XmlWriter writer, string name) + { + writer.WriteStartElement("member"); + writer.WriteElementString("name", name); + writer.WriteStartElement("value"); + writer.WriteStartElement("array"); + writer.WriteStartElement("data"); + } + + public static void WriteXmlRpcArrayEnd(XmlWriter writer) + { + writer.WriteEndElement(); + writer.WriteEndElement(); + writer.WriteEndElement(); + writer.WriteEndElement(); + } + + #endregion XmlRpc Serializing Helpers } #endregion Structs @@ -656,6 +888,8 @@ namespace OpenMetaverse bool loginSuccess = llsd.AsBoolean(); bool redirect = (llsd.AsString() == "indeterminate"); LoginResponseData data = new LoginResponseData(); + data.Reason = reason; + data.Message = message; if (redirect) { diff --git a/OpenMetaverse/NetworkManager.cs b/OpenMetaverse/NetworkManager.cs index 9b649e72..0fcdaf9f 100644 --- a/OpenMetaverse/NetworkManager.cs +++ b/OpenMetaverse/NetworkManager.cs @@ -776,20 +776,14 @@ namespace OpenMetaverse #endregion ACK handling } - #region FireCallbacks + #region Fire callbacks if (Client.Settings.SYNC_PACKETCALLBACKS) - { - PacketEvents.RaiseEvent(PacketType.Default, packet, simulator); PacketEvents.RaiseEvent(packet.Type, packet, simulator); - } else - { - PacketEvents.BeginRaiseEvent(PacketType.Default, packet, simulator); PacketEvents.BeginRaiseEvent(packet.Type, packet, simulator); - } - #endregion FireCallbacks + #endregion Fire callbacks } } } diff --git a/OpenMetaverse/Simulator.cs b/OpenMetaverse/Simulator.cs index d254f0eb..f6d9a228 100644 --- a/OpenMetaverse/Simulator.cs +++ b/OpenMetaverse/Simulator.cs @@ -855,7 +855,7 @@ namespace OpenMetaverse } Stats.RecvBytes += (ulong)buffer.DataLength; - Stats.RecvPackets++; + ++Stats.RecvPackets; #endregion Packet Decoding diff --git a/Programs/Simian/Agent.cs b/Programs/Simian/Agent.cs new file mode 100644 index 00000000..9ad06067 --- /dev/null +++ b/Programs/Simian/Agent.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using OpenMetaverse; +using OpenMetaverse.Packets; + +namespace Simian +{ + public class Agent + { + public UUID AgentID; + public UUID SessionID; + public UUID SecureSessionID; + public uint CircuitCode; + + /// Sequence numbers of packets we've received (for duplicate checking) + internal Queue packetArchive = new Queue(); + /// Packets we have sent that need to be ACKed by the client + internal Dictionary needAcks = new Dictionary(); + + UDPServer udpServer; + IPEndPoint address; + /// ACKs that are queued up, waiting to be sent to the client + SortedList pendingAcks = new SortedList(); + int currentSequence = 0; + Timer ackTimer; + + public IPEndPoint Address + { + get { return address; } + set { address = value; } + } + + public Agent(UDPServer udpServer, UUID agentID, UUID sessionID, UUID secureSessionID, uint circuitCode) + { + AgentID = agentID; + SessionID = sessionID; + SecureSessionID = secureSessionID; + CircuitCode = circuitCode; + + this.udpServer = udpServer; + } + + public void Initialize(IPEndPoint address) + { + this.address = address; + + ackTimer = new Timer(new TimerCallback(AckTimer_Elapsed), null, Settings.NETWORK_TICK_INTERVAL, + Settings.NETWORK_TICK_INTERVAL); + } + + public void SendPacket(Packet packet) + { + SendPacket(packet, true); + } + + public void SendPacket(Packet packet, bool setSequence) + { + byte[] buffer; + int bytes; + + // Keep track of when this packet was sent out + packet.TickCount = Environment.TickCount; + + if (setSequence) + { + // Reset to zero if we've hit the upper sequence number limit + Interlocked.CompareExchange(ref currentSequence, 0, 0xFFFFFF); + // Increment and fetch the current sequence number + uint sequence = (uint)Interlocked.Increment(ref currentSequence); + packet.Header.Sequence = sequence; + + if (packet.Header.Reliable) + { + // Add this packet to the list of ACK responses we are waiting on from the client + lock (needAcks) + needAcks[sequence] = packet; + + if (packet.Header.Resent) + { + // This packet has already been sent out once, strip any appended ACKs + // off it and reinsert them into the outgoing ACK queue under the + // assumption that this packet will continually be rejected from the + // client or that the appended ACKs are possibly making the delivery fail + if (packet.Header.AckList.Length > 0) + { + Logger.DebugLog(String.Format("Purging ACKs from packet #{0} ({1}) which will be resent.", + packet.Header.Sequence, packet.GetType())); + + lock (pendingAcks) + { + foreach (uint ack in packet.Header.AckList) + { + if (!pendingAcks.ContainsKey(ack)) + pendingAcks[ack] = ack; + } + } + + packet.Header.AppendedAcks = false; + packet.Header.AckList = new uint[0]; + } + } + else + { + // This packet is not a resend, check if the conditions are favorable + // to ACK appending + if (packet.Type != PacketType.PacketAck) + { + lock (pendingAcks) + { + if (pendingAcks.Count > 0 && + pendingAcks.Count < 10) + { + // Append all of the queued up outgoing ACKs to this packet + packet.Header.AckList = new uint[pendingAcks.Count]; + + for (int i = 0; i < pendingAcks.Count; i++) + packet.Header.AckList[i] = pendingAcks.Values[i]; + + pendingAcks.Clear(); + packet.Header.AppendedAcks = true; + } + } + } + } + } + else if (packet.Header.AckList.Length > 0) + { + // Sanity check for ACKS appended on an unreliable packet, this is bad form + Logger.Log("Sending appended ACKs on an unreliable packet", Helpers.LogLevel.Warning); + } + } + + // Serialize the packet + buffer = packet.ToBytes(); + bytes = buffer.Length; + //Stats.SentBytes += (ulong)bytes; + //++Stats.SentPackets; + + UDPPacketBuffer buf; + + // Zerocode if needed + if (packet.Header.Zerocoded) + { + buf = new UDPPacketBuffer(address, true, false); + + bytes = Helpers.ZeroEncode(buffer, bytes, buf.Data); + buf.DataLength = bytes; + } + else + { + buf = new UDPPacketBuffer(address, false, false); + + buf.Data = buffer; + buf.DataLength = bytes; + } + + udpServer.AsyncBeginSend(buf); + } + + public void QueueAck(uint ack) + { + // Add this packet to the list of ACKs that need to be sent out + lock (pendingAcks) + pendingAcks[ack] = ack; + + // Send out ACKs if we have a lot of them + if (pendingAcks.Count >= 10) + SendAcks(); + } + + public void ProcessAcks(List acks) + { + lock (needAcks) + { + foreach (uint ack in acks) + needAcks.Remove(ack); + } + } + + public void SendAck(uint ack) + { + PacketAckPacket acks = new PacketAckPacket(); + acks.Header.Reliable = false; + acks.Packets = new PacketAckPacket.PacketsBlock[1]; + acks.Packets[0] = new PacketAckPacket.PacketsBlock(); + acks.Packets[0].ID = ack; + + SendPacket(acks, true); + } + + void SendAcks() + { + PacketAckPacket acks = null; + + lock (pendingAcks) + { + if (pendingAcks.Count > 0) + { + if (pendingAcks.Count > 250) + { + Logger.Log("Too many ACKs queued up!", Helpers.LogLevel.Error); + return; + } + + acks = new PacketAckPacket(); + acks.Header.Reliable = false; + acks.Packets = new PacketAckPacket.PacketsBlock[pendingAcks.Count]; + + for (int i = 0; i < pendingAcks.Count; i++) + { + acks.Packets[i] = new PacketAckPacket.PacketsBlock(); + acks.Packets[i].ID = pendingAcks.Values[i]; + } + + pendingAcks.Clear(); + } + } + + if (acks != null) + SendPacket(acks, true); + } + + void ResendUnacked() + { + lock (needAcks) + { + List dropAck = new List(); + int now = Environment.TickCount; + + // Resend packets + foreach (Packet packet in needAcks.Values) + { + if (packet.TickCount != 0 && now - packet.TickCount > 4000) + { + if (packet.ResendCount < 3) + { + Logger.DebugLog(String.Format("Resending packet #{0} ({1}), {2}ms have passed", + packet.Header.Sequence, packet.GetType(), now - packet.TickCount)); + + packet.TickCount = 0; + packet.Header.Resent = true; + //++Stats.ResentPackets; + ++packet.ResendCount; + + SendPacket(packet, false); + } + else + { + Logger.Log(String.Format("Dropping packet #{0} ({1}) after {2} failed attempts", + packet.Header.Sequence, packet.GetType(), packet.ResendCount), Helpers.LogLevel.Warning); + + dropAck.Add(packet.Header.Sequence); + } + } + } + + if (dropAck.Count != 0) + { + foreach (uint seq in dropAck) + needAcks.Remove(seq); + } + } + } + + private void AckTimer_Elapsed(object obj) + { + SendAcks(); + ResendUnacked(); + } + } +} diff --git a/Programs/Simian/EventDictionary.cs b/Programs/Simian/EventDictionary.cs new file mode 100644 index 00000000..6941e5b4 --- /dev/null +++ b/Programs/Simian/EventDictionary.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using OpenMetaverse; +using OpenMetaverse.Packets; + +namespace Simian +{ + /// + /// Registers, unregisters, and fires events generated by incoming packets + /// + public class PacketEventDictionary + { + /// + /// Object that is passed to worker threads in the ThreadPool for + /// firing packet callbacks + /// + private struct PacketCallbackWrapper + { + /// Callback to fire for this packet + public UDPServer.PacketCallback Callback; + /// Reference to the agent that this packet came from + public Agent Agent; + /// The packet that needs to be processed + public Packet Packet; + } + + private Dictionary _EventTable = new Dictionary(); + private WaitCallback _ThreadPoolCallback; + + /// + /// Default constructor + /// + public PacketEventDictionary() + { + _ThreadPoolCallback = new WaitCallback(ThreadPoolDelegate); + } + + /// + /// Register an event handler + /// + /// Use PacketType.Default to fire this event on every + /// incoming packet + /// Packet type to register the handler for + /// Callback to be fired + public void RegisterEvent(PacketType packetType, UDPServer.PacketCallback eventHandler) + { + lock (_EventTable) + { + if (_EventTable.ContainsKey(packetType)) + _EventTable[packetType] += eventHandler; + else + _EventTable[packetType] = eventHandler; + } + } + + /// + /// Unregister an event handler + /// + /// Packet type to unregister the handler for + /// Callback to be unregistered + public void UnregisterEvent(PacketType packetType, UDPServer.PacketCallback eventHandler) + { + lock (_EventTable) + { + if (_EventTable.ContainsKey(packetType) && _EventTable[packetType] != null) + _EventTable[packetType] -= eventHandler; + } + } + + /// + /// Fire the events registered for this packet type asynchronously + /// + /// Incoming packet type + /// Incoming packet + /// Agent this packet was received from + internal void BeginRaiseEvent(PacketType packetType, Packet packet, Agent agent) + { + UDPServer.PacketCallback callback; + PacketCallbackWrapper wrapper; + + // Default handler first, if one exists + if (_EventTable.TryGetValue(PacketType.Default, out callback)) + { + if (callback != null) + { + wrapper.Callback = callback; + wrapper.Packet = packet; + wrapper.Agent = agent; + ThreadPool.QueueUserWorkItem(_ThreadPoolCallback, wrapper); + } + } + + if (_EventTable.TryGetValue(packetType, out callback)) + { + if (callback != null) + { + wrapper.Callback = callback; + wrapper.Packet = packet; + wrapper.Agent = agent; + ThreadPool.QueueUserWorkItem(_ThreadPoolCallback, wrapper); + + return; + } + } + + if (packetType != PacketType.Default && packetType != PacketType.PacketAck) + { + Logger.DebugLog("No handler registered for packet event " + packetType); + } + } + + private void ThreadPoolDelegate(Object state) + { + PacketCallbackWrapper wrapper = (PacketCallbackWrapper)state; + + try + { + wrapper.Callback(wrapper.Packet, wrapper.Agent); + } + catch (Exception ex) + { + Logger.Log("Async Packet Event Handler: " + ex.ToString(), Helpers.LogLevel.Error); + } + } + } +} diff --git a/Programs/Simian/Main.cs b/Programs/Simian/Main.cs new file mode 100644 index 00000000..bd64e1cd --- /dev/null +++ b/Programs/Simian/Main.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Simian +{ + class MainEntry + { + static void Main(string[] args) + { + Simian simulator = new Simian(); + simulator.Start(9000, false); + + Console.WriteLine("Simulator is running. Press ENTER to quit"); + + Console.ReadLine(); + + simulator.Stop(); + } + } +} diff --git a/Programs/Simian/PacketHandlers.cs b/Programs/Simian/PacketHandlers.cs new file mode 100644 index 00000000..783245d5 --- /dev/null +++ b/Programs/Simian/PacketHandlers.cs @@ -0,0 +1,75 @@ +using System; +using OpenMetaverse; +using OpenMetaverse.Packets; + +namespace Simian +{ + public partial class Simian + { + void UseCircuitCodeHandler(Packet packet, Agent agent) + { + RegionHandshakePacket handshake = new RegionHandshakePacket(); + handshake.RegionInfo.BillableFactor = 0f; + handshake.RegionInfo.CacheID = UUID.Random(); + handshake.RegionInfo.IsEstateManager = false; + handshake.RegionInfo.RegionFlags = 1; + handshake.RegionInfo.SimOwner = UUID.Random(); + handshake.RegionInfo.SimAccess = 1; + handshake.RegionInfo.SimName = Utils.StringToBytes("Simian"); + handshake.RegionInfo.WaterHeight = 20.0f; + handshake.RegionInfo.TerrainBase0 = UUID.Zero; + handshake.RegionInfo.TerrainBase1 = UUID.Zero; + handshake.RegionInfo.TerrainBase2 = UUID.Zero; + handshake.RegionInfo.TerrainBase3 = UUID.Zero; + handshake.RegionInfo.TerrainDetail0 = UUID.Zero; + handshake.RegionInfo.TerrainDetail1 = UUID.Zero; + handshake.RegionInfo.TerrainDetail2 = UUID.Zero; + handshake.RegionInfo.TerrainDetail3 = UUID.Zero; + handshake.RegionInfo.TerrainHeightRange00 = 0f; + handshake.RegionInfo.TerrainHeightRange01 = 20f; + handshake.RegionInfo.TerrainHeightRange10 = 0f; + handshake.RegionInfo.TerrainHeightRange11 = 20f; + handshake.RegionInfo.TerrainStartHeight00 = 0f; + handshake.RegionInfo.TerrainStartHeight01 = 40f; + handshake.RegionInfo.TerrainStartHeight10 = 0f; + handshake.RegionInfo.TerrainStartHeight11 = 40f; + handshake.RegionInfo2.RegionID = UUID.Random(); + + agent.SendPacket(handshake); + } + + void StartPingCheckHandler(Packet packet, Agent agent) + { + StartPingCheckPacket start = (StartPingCheckPacket)packet; + + CompletePingCheckPacket complete = new CompletePingCheckPacket(); + complete.Header.Reliable = false; + complete.PingID.PingID = start.PingID.PingID; + + agent.SendPacket(complete); + } + + void CompleteAgentMovementHandler(Packet packet, Agent agent) + { + uint regionX = 256000; + uint regionY = 256000; + + CompleteAgentMovementPacket request = (CompleteAgentMovementPacket)packet; + + AgentMovementCompletePacket complete = new AgentMovementCompletePacket(); + complete.AgentData.AgentID = agent.AgentID; + complete.AgentData.SessionID = agent.SessionID; + complete.Data.LookAt = Vector3.UnitX; + complete.Data.Position = new Vector3(128f, 128f, 25f); + complete.Data.RegionHandle = Helpers.UIntsToLong(regionX, regionY); + complete.Data.Timestamp = Utils.DateTimeToUnixTime(DateTime.Now); + complete.SimData.ChannelVersion = Utils.StringToBytes("Simian"); + + agent.SendPacket(complete); + } + + void AgentUpdateHandler(Packet packet, Agent agent) + { + } + } +} diff --git a/Programs/Simian/Simian.cs b/Programs/Simian/Simian.cs new file mode 100644 index 00000000..b5b39675 --- /dev/null +++ b/Programs/Simian/Simian.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.IO; +using System.Text; +using System.Xml; +using System.Threading; +using OpenMetaverse; +using OpenMetaverse.Capabilities; +using OpenMetaverse.Packets; + +namespace Simian +{ + public partial class Simian + { + HttpServer httpServer; + UDPServer udpServer; + Dictionary unassociatedAgents; + int currentCircuitCode; + int tcpPort; + int udpPort; + + public Simian() + { + unassociatedAgents = new Dictionary(); + currentCircuitCode = 0; + } + + public void Start(int port, bool ssl) + { + // Put UDP listening on the same port number as the HTTP server for simplicity + tcpPort = port; + udpPort = port; + + InitUDPServer(udpPort); + InitHttpServer(tcpPort, ssl); + } + + public void Stop() + { + udpServer.Stop(); + httpServer.Stop(); + } + + public bool TryGetUnassociatedAgent(uint circuitCode, out Agent agent) + { + if (unassociatedAgents.TryGetValue(circuitCode, out agent)) + { + lock (unassociatedAgents) + unassociatedAgents.Remove(circuitCode); + + return true; + } + else + { + return false; + } + } + + void InitUDPServer(int port) + { + udpServer = new UDPServer(port, this); + + udpServer.RegisterPacketCallback(PacketType.UseCircuitCode, new UDPServer.PacketCallback(UseCircuitCodeHandler)); + udpServer.RegisterPacketCallback(PacketType.StartPingCheck, new UDPServer.PacketCallback(StartPingCheckHandler)); + udpServer.RegisterPacketCallback(PacketType.CompleteAgentMovement, new UDPServer.PacketCallback(CompleteAgentMovementHandler)); + udpServer.RegisterPacketCallback(PacketType.AgentUpdate, new UDPServer.PacketCallback(AgentUpdateHandler)); + } + + void InitHttpServer(int port, bool ssl) + { + httpServer = new HttpServer(tcpPort, ssl); + + // Login webpage HEAD request, used to check if the login webpage is alive + HttpRequestSignature signature = new HttpRequestSignature(); + signature.Method = "head"; + signature.ContentType = String.Empty; + signature.Path = "/loginpage"; + HttpServer.HttpRequestCallback callback = new HttpServer.HttpRequestCallback(LoginWebpageHeadHandler); + HttpServer.HttpRequestHandler handler = new HttpServer.HttpRequestHandler(signature, callback); + httpServer.AddHandler(handler); + + // Login webpage GET request, gets the login webpage data (purely aesthetic) + signature.Method = "get"; + signature.ContentType = String.Empty; + signature.Path = "/loginpage"; + callback = new HttpServer.HttpRequestCallback(LoginWebpageGetHandler); + handler.Signature = signature; + handler.Callback = callback; + httpServer.AddHandler(handler); + + // Client XML-RPC login + signature.Method = "post"; + signature.ContentType = "text/xml"; + signature.Path = "/login"; + callback = new HttpServer.HttpRequestCallback(LoginXmlRpcPostHandler); + handler.Signature = signature; + handler.Callback = callback; + httpServer.AddHandler(handler); + + // Client LLSD login + signature.Method = "post"; + signature.ContentType = "application/xml"; + signature.Path = "/login"; + callback = new HttpServer.HttpRequestCallback(LoginLLSDPostHandler); + handler.Signature = signature; + handler.Callback = callback; + httpServer.AddHandler(handler); + + httpServer.Start(); + } + + void LoginWebpageHeadHandler(ref HttpListenerContext context) + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + context.Response.StatusDescription = "OK"; + } + + void LoginWebpageGetHandler(ref HttpListenerContext context) + { + string pageContent = "Simian

Welcome to Simian

"; + byte[] pageData = Encoding.UTF8.GetBytes(pageContent); + context.Response.OutputStream.Write(pageData, 0, pageData.Length); + context.Response.Close(); + } + + void LoginXmlRpcPostHandler(ref HttpListenerContext context) + { + string + firstName = String.Empty, + lastName = String.Empty, + password = String.Empty, + start = String.Empty, + version = String.Empty, + channel = String.Empty; + + try + { + // Parse the incoming XML + XmlReader reader = XmlReader.Create(context.Request.InputStream); + + reader.ReadStartElement("methodCall"); + { + string methodName = reader.ReadElementContentAsString("methodName", String.Empty); + + if (methodName == "login_to_simulator") + { + reader.ReadStartElement("params"); + reader.ReadStartElement("param"); + reader.ReadStartElement("value"); + reader.ReadStartElement("struct"); + { + while (reader.Name == "member") + { + reader.ReadStartElement("member"); + { + string name = reader.ReadElementContentAsString("name", String.Empty); + + reader.ReadStartElement("value"); + { + switch (name) + { + case "first": + firstName = reader.ReadElementContentAsString("string", String.Empty); + break; + case "last": + lastName = reader.ReadElementContentAsString("string", String.Empty); + break; + case "passwd": + password = reader.ReadElementContentAsString("string", String.Empty); + break; + case "start": + start = reader.ReadElementContentAsString("string", String.Empty); + break; + case "version": + version = reader.ReadElementContentAsString("string", String.Empty); + break; + case "channel": + channel = reader.ReadElementContentAsString("string", String.Empty); + break; + default: + if (reader.Name == "string") + Console.WriteLine(String.Format("Ignore login xml value: name={0}, value={1}", name, reader.ReadInnerXml())); + else + Console.WriteLine(String.Format("Unknown login xml: name={0}, value={1}", name, reader.ReadInnerXml())); + break; + } + } + reader.ReadEndElement(); + } + reader.ReadEndElement(); + } + } + reader.ReadEndElement(); + reader.ReadEndElement(); + reader.ReadEndElement(); + reader.ReadEndElement(); + } + } + reader.ReadEndElement(); + reader.Close(); + + LoginResponseData responseData = HandleLogin(firstName, lastName, password, start, version, channel); + XmlWriter writer = XmlWriter.Create(context.Response.OutputStream); + responseData.ToXmlRpc(writer); + writer.Close(); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + } + } + + void LoginLLSDPostHandler(ref HttpListenerContext context) + { + string body = String.Empty; + + using (StreamReader reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding)) + { + body = reader.ReadToEnd(); + } + + Console.WriteLine(body); + } + + LoginResponseData HandleLogin(string firstName, string lastName, string password, string start, string version, string channel) + { + uint regionX = 256000; + uint regionY = 256000; + + // Setup default login response values + LoginResponseData response; + + response.AgentID = UUID.Random(); + response.SecureSessionID = UUID.Random(); + response.SessionID = UUID.Random(); + response.CircuitCode = CreateAgentCircuit(response.AgentID, response.SessionID, response.SecureSessionID); + response.AgentAccess = "M"; + response.BuddyList = null; + response.FirstName = firstName; + response.HomeLookAt = Vector3.UnitX; + response.HomePosition = new Vector3(128f, 128f, 25f); + response.HomeRegion = Helpers.UIntsToLong(regionX, regionY); + response.InventoryFolders = null; + response.InventoryRoot = UUID.Random(); + response.LastName = lastName; + response.LibraryFolders = null; + response.LibraryOwner = response.AgentID; + response.LibraryRoot = UUID.Random(); + response.LookAt = Vector3.UnitX; + response.Message = "Welcome to Simian"; + response.Reason = String.Empty; + response.RegionX = regionX; + response.RegionY = regionY; + response.SecondsSinceEpoch = DateTime.Now; + // FIXME: Actually generate a seed capability + response.SeedCapability = String.Format("http://{0}:{1}/seed_caps", IPAddress.Loopback, tcpPort); + response.SimIP = IPAddress.Loopback; + response.SimPort = (ushort)udpPort; + response.StartLocation = "last"; + response.Success = true; + + return response; + } + + uint CreateAgentCircuit(UUID agentID, UUID sessionID, UUID secureSessionID) + { + uint circuitCode = (uint)Interlocked.Increment(ref currentCircuitCode); + + Agent agent = new Agent(udpServer, agentID, sessionID, secureSessionID, circuitCode); + + // Put this client in the list of clients that have not been associated with an IPEndPoint yet + lock (unassociatedAgents) + unassociatedAgents[circuitCode] = agent; + + Logger.Log("Created a circuit for agent " + agentID.ToString(), Helpers.LogLevel.Info); + + return circuitCode; + } + } +} diff --git a/Programs/Simian/UDPServer.cs b/Programs/Simian/UDPServer.cs new file mode 100644 index 00000000..ec3527ab --- /dev/null +++ b/Programs/Simian/UDPServer.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using OpenMetaverse; +using OpenMetaverse.Packets; + +namespace Simian +{ + public struct IncomingPacket + { + public Agent Agent; + public Packet Packet; + } + + public class UDPServer : UDPBase + { + /// + /// Coupled with RegisterCallback(), this is triggered whenever a packet + /// of a registered type is received + /// + public delegate void PacketCallback(Packet packet, Agent agent); + + /// This is only used to fetch unassociated agents, which will + /// be exposed through a login interface at some point + Simian server; + /// Handlers for incoming packets + PacketEventDictionary packetEvents = new PacketEventDictionary(); + /// All of the agents currently connected to this UDP server + Dictionary agents = new Dictionary(); + /// Incoming packets that are awaiting handling + BlockingQueue packetInbox = new BlockingQueue(Settings.PACKET_INBOX_SIZE); + + public UDPServer(int port, Simian server) + : base(port) + { + this.server = server; + + Start(); + + // Start the incoming packet processing thread + Thread incomingThread = new Thread(new ThreadStart(IncomingPacketHandler)); + incomingThread.Start(); + } + + public void RegisterPacketCallback(PacketType type, PacketCallback callback) + { + packetEvents.RegisterEvent(type, callback); + } + + protected override void PacketReceived(UDPPacketBuffer buffer) + { + Agent agent = null; + Packet packet = null; + int packetEnd = buffer.DataLength - 1; + + // Decoding + try + { + packet = Packet.BuildPacket(buffer.Data, ref packetEnd, buffer.ZeroData); + } + catch (MalformedDataException) + { + Logger.Log(String.Format("Malformed data, cannot parse packet:\n{0}", + Utils.BytesToHexString(buffer.Data, buffer.DataLength, null)), Helpers.LogLevel.Error); + } + + // Fail-safe check + if (packet == null) + { + Logger.Log("Couldn't build a message from the incoming data", Helpers.LogLevel.Warning); + return; + } + + //Stats.RecvBytes += (ulong)buffer.DataLength; + //++Stats.RecvPackets; + + if (packet.Type == PacketType.UseCircuitCode) + { + UseCircuitCodePacket useCircuitCode = (UseCircuitCodePacket)packet; + + if (server.TryGetUnassociatedAgent(useCircuitCode.CircuitCode.Code, out agent)) + { + agent.Initialize((IPEndPoint)buffer.RemoteEndPoint); + + lock (agents) + agents[(IPEndPoint)buffer.RemoteEndPoint] = agent; + + Logger.Log("Activated UDP circuit " + useCircuitCode.CircuitCode.Code, Helpers.LogLevel.Info); + + //agent.SendAck(useCircuitCode.Header.Sequence); + } + else + { + Logger.Log("Received a UseCircuitCode packet for an unrecognized circuit: " + useCircuitCode.CircuitCode.Code.ToString(), + Helpers.LogLevel.Warning); + return; + } + } + else + { + // Determine which agent this packet came from + if (!agents.TryGetValue((IPEndPoint)buffer.RemoteEndPoint, out agent)) + { + Logger.Log("Received UDP packet from an unrecognized source: " + ((IPEndPoint)buffer.RemoteEndPoint).ToString(), + Helpers.LogLevel.Warning); + return; + } + } + + // Reliable handling + if (packet.Header.Reliable) + { + // Queue up this sequence number for acknowledgement + agent.QueueAck((uint)packet.Header.Sequence); + + //if (packet.Header.Resent) ++Stats.ReceivedResends; + } + + // Inbox insertion + IncomingPacket incomingPacket; + incomingPacket.Agent = agent; + incomingPacket.Packet = packet; + + // TODO: Prioritize the queue + packetInbox.Enqueue(incomingPacket); + } + + protected override void PacketSent(UDPPacketBuffer buffer, int bytesSent) + { + Logger.DebugLog("Sent " + buffer.DataLength + " byte packet"); + } + + private void IncomingPacketHandler() + { + IncomingPacket incomingPacket = new IncomingPacket(); + Packet packet = null; + Agent agent = null; + + while (IsRunning) + { + // Reset packet to null for the check below + packet = null; + + if (packetInbox.Dequeue(100, ref incomingPacket)) + { + packet = incomingPacket.Packet; + agent = incomingPacket.Agent; + + if (packet != null) + { + #region ACK accounting + + // Check the archives to see whether we already received this packet + lock (agent.packetArchive) + { + if (agent.packetArchive.Contains(packet.Header.Sequence)) + { + if (packet.Header.Resent) + { + Logger.DebugLog("Received resent packet #" + packet.Header.Sequence); + } + else + { + Logger.Log(String.Format("Received a duplicate of packet #{0}, current type: {1}", + packet.Header.Sequence, packet.Type), Helpers.LogLevel.Warning); + } + + // Avoid firing a callback twice for the same packet + continue; + } + else + { + // Keep the PacketArchive size within a certain capacity + while (agent.packetArchive.Count >= Settings.PACKET_ARCHIVE_SIZE) + { + agent.packetArchive.Dequeue(); agent.packetArchive.Dequeue(); + agent.packetArchive.Dequeue(); agent.packetArchive.Dequeue(); + } + + agent.packetArchive.Enqueue(packet.Header.Sequence); + } + } + + #endregion ACK accounting + + #region ACK handling + + // Handle appended ACKs + if (packet.Header.AppendedAcks) + { + lock (agent.needAcks) + { + for (int i = 0; i < packet.Header.AckList.Length; i++) + agent.needAcks.Remove(packet.Header.AckList[i]); + } + } + + // Handle PacketAck packets + if (packet.Type == PacketType.PacketAck) + { + PacketAckPacket ackPacket = (PacketAckPacket)packet; + + lock (agent.needAcks) + { + for (int i = 0; i < ackPacket.Packets.Length; i++) + agent.needAcks.Remove(ackPacket.Packets[i].ID); + } + } + + #endregion ACK handling + + packetEvents.BeginRaiseEvent(packet.Type, packet, agent); + } + } + } + } + } +} diff --git a/prebuild.xml b/prebuild.xml index 905b2f24..cf7388a0 100644 --- a/prebuild.xml +++ b/prebuild.xml @@ -664,6 +664,29 @@ + + + + + ../../bin/ + + + + + ../../bin/ + + + + ../../bin/ + + + + + + + + +