using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net.Sockets; using Tor.Helpers; using System.ComponentModel; using System.Reflection; using System.Diagnostics; namespace Tor.Events { /// /// A class which provides monitoring for events occuring within the tor service. /// public sealed class Events : IDisposable { private readonly static Dictionary dispatchers; private readonly static string EOL = "\r\n"; private readonly Client client; private readonly Dictionary events; private readonly object synchronize; private StringBuilder backlog; private Action connectCallback; private StringBuilder multiLine; private byte[] buffer; private volatile bool disposed; private volatile bool enabled; private Socket socket; private event BandwidthEventHandler bandwidthChangedHandlers; private event CircuitEventHandler circuitChangedHandlers; private event ConfigurationChangedEventHandler configurationChangedHandlers; private event LogEventHandler debugReceivedHandlers; private event LogEventHandler errorReceivedHandlers; private event LogEventHandler infoReceivedHandlers; private event LogEventHandler noticeReceivedHandlers; private event ORConnectionEventHandler orConnectionChangedHandlers; private event StreamEventHandler streamChangedHandlers; private event LogEventHandler warnReceivedHandlers; /// /// Initializes the class. /// static Events() { dispatchers = new Dictionary(); Event[] values = Enum.GetValues(typeof(Event)) as Event[]; foreach (Type type in Assembly.GetExecutingAssembly().GetTypes().Where(t => t.Namespace == "Tor.Events" && typeof(Dispatcher).IsAssignableFrom(t))) { EventAssocAttribute attribute = Attribute.GetCustomAttribute(type, typeof(EventAssocAttribute)) as EventAssocAttribute; if (attribute == null) continue; foreach (Event value in values) { if ((attribute.Event & value) == value) dispatchers[value] = type; } } } /// /// Initializes a new instance of the class. /// /// The client for which this object instance belongs. internal Events(Client client) { this.backlog = new StringBuilder(); this.buffer = new byte[256]; this.connectCallback = null; this.client = client; this.disposed = false; this.enabled = true; this.events = new Dictionary(); this.multiLine = null; this.socket = null; this.synchronize = new object(); } #region Events /// /// Occurs when the bandwidth download and upload values change within the tor service. /// public event BandwidthEventHandler BandwidthChanged { add { bandwidthChangedHandlers += value; AddEvent(Event.Bandwidth); } remove { bandwidthChangedHandlers += value; RemoveEvent(Event.Bandwidth); } } /// /// Occurs when a circuit has changed within the tor service. /// public event CircuitEventHandler CircuitChanged { add { circuitChangedHandlers += value; AddEvent(Event.Circuits); } remove { circuitChangedHandlers -= value; RemoveEvent(Event.Circuits); } } /// /// Occurs when configurations have changed within the tor service. /// public event ConfigurationChangedEventHandler ConfigurationChanged { add { configurationChangedHandlers += value; AddEvent(Event.ConfigChanged); } remove { configurationChangedHandlers -= value; RemoveEvent(Event.ConfigChanged); } } /// /// Occurs when a debug log message is received. /// public event LogEventHandler DebugReceived { add { debugReceivedHandlers += value; AddEvent(Event.LogDebug); } remove { debugReceivedHandlers -= value; RemoveEvent(Event.LogDebug); } } /// /// Occurs when an error log message is received. /// public event LogEventHandler ErrorReceived { add { errorReceivedHandlers += value; AddEvent(Event.LogError); } remove { errorReceivedHandlers -= value; RemoveEvent(Event.LogError); } } /// /// Occurs when an information log message is received. /// public event LogEventHandler InfoReceived { add { infoReceivedHandlers += value; AddEvent(Event.LogInfo); } remove { infoReceivedHandlers -= value; RemoveEvent(Event.LogInfo); } } /// /// Occurs when a notice log message is received. /// public event LogEventHandler NoticeReceived { add { noticeReceivedHandlers += value; AddEvent(Event.LogNotice); } remove { noticeReceivedHandlers -= value; RemoveEvent(Event.LogNotice); } } /// /// Occurs when an OR connection has changed within the tor service. /// public event ORConnectionEventHandler ORConnectionChanged { add { orConnectionChangedHandlers += value; AddEvent(Event.ORConnections); } remove { orConnectionChangedHandlers -= value; RemoveEvent(Event.ORConnections); } } /// /// Occurs when a stream has changed within the tor service. /// public event StreamEventHandler StreamChanged { add { streamChangedHandlers += value; AddEvent(Event.Streams); } remove { streamChangedHandlers -= value; RemoveEvent(Event.Streams); } } /// /// Occurs when a warning log message is received. /// public event LogEventHandler WarnReceived { add { warnReceivedHandlers += value; AddEvent(Event.LogWarn); } remove { warnReceivedHandlers -= value; RemoveEvent(Event.LogWarn); } } #endregion #region Events Invoke /// /// Raises the event. /// /// The instance containing the event data. internal void OnBandwidthChanged(BandwidthEventArgs e) { if (bandwidthChangedHandlers != null) bandwidthChangedHandlers(client, e); } /// /// Raises the event. /// /// The instance containing the event data. internal void OnCircuitChanged(CircuitEventArgs e) { if (circuitChangedHandlers != null) circuitChangedHandlers(client, e); } /// /// Raises the event. /// /// The instance containing the event data. internal void OnConfigurationChanged(ConfigurationChangedEventArgs e) { if (configurationChangedHandlers != null) configurationChangedHandlers(client, e); } /// /// Raises the event. /// /// The instance containing the event data. /// The event which was processed. internal void OnLogReceived(LogEventArgs e, Event evt) { switch (evt) { case Event.LogDebug: if (debugReceivedHandlers != null) debugReceivedHandlers(client, e); break; case Event.LogError: if (errorReceivedHandlers != null) errorReceivedHandlers(client, e); break; case Event.LogInfo: if (infoReceivedHandlers != null) infoReceivedHandlers(client, e); break; case Event.LogNotice: if (noticeReceivedHandlers != null) noticeReceivedHandlers(client, e); break; case Event.LogWarn: if (warnReceivedHandlers != null) warnReceivedHandlers(client, e); break; } } /// /// Raises the event. /// /// The instance containing the event data. internal void OnORConnectionChanged(ORConnectionEventArgs e) { if (orConnectionChangedHandlers != null) orConnectionChangedHandlers(client, e); } /// /// Raises the event. /// /// The instance containing the event data. internal void OnStreamChanged(StreamEventArgs e) { if (streamChangedHandlers != null) streamChangedHandlers(client, e); } #endregion #region System.IDisposable /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. private void Dispose(bool disposing) { if (disposed) return; if (disposing) { Shutdown(); disposed = true; } } #endregion #region System.Net.Sockets.Socket /// /// Called when the socket connects to the control port. /// /// The asynchronous result object for the asynchronous method. private void OnSocketConnect(IAsyncResult ar) { try { socket.EndConnect(ar); string authenticate = string.Format("authenticate \"{0}\"{1}", client.GetControlPassword(), EOL); byte[] authenticateBuffer = Encoding.ASCII.GetBytes(authenticate); socket.BeginSend(authenticateBuffer, 0, authenticateBuffer.Length, SocketFlags.None, OnSocketSendAuthenticate, null); } catch { } } /// /// Called when the socket receives a response to the authentication command. /// /// The asynchronous result object for the asynchronous method. private void OnSocketReceiveAuthenticate(IAsyncResult ar) { try { int received = socket.EndReceive(ar); if (received <= 0) return; string response = Encoding.ASCII.GetString(buffer, 0, received); if (response.Length < 3 || !response.Substring(0, 3).Equals("250")) throw new TorException("The events manager failed to authenticate with the control connection"); if (connectCallback != null) connectCallback(); SetEvents(); socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, OnSocketReceive, null); } catch { } } /// /// Called when the socket receives data from the control port. /// /// The asynchronous result object for the asynchronous method. private void OnSocketReceive(IAsyncResult ar) { try { int received = socket.EndReceive(ar); if (received <= 0) return; string response = Encoding.ASCII.GetString(buffer, 0, received); SetEventResponse(response); socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, OnSocketReceive, null); } catch { } } /// /// Called when the socket has completed sending the authentication command. /// /// The asynchronous result object for the asynchronous method. private void OnSocketSendAuthenticate(IAsyncResult ar) { try { socket.EndSend(ar); socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, OnSocketReceiveAuthenticate, null); } catch { } } /// /// Called when the socket completes sending a list of interested events. /// /// The asynchronous result object for the asynchronous method. private void OnSocketSendEvents(IAsyncResult ar) { try { socket.EndSend(ar); } catch { } } #endregion /// /// Adds an event to the list of monitored events. /// /// The event which should be monitored for. private void AddEvent(Event evt) { if (disposed) throw new ObjectDisposedException("this"); lock (synchronize) { int counter = 0; events.TryGetValue(evt, out counter); events[evt] = ++counter; if (enabled && counter == 1) SetEvents(); } } /// /// Dispatches an event by parsing the content of the line and raising the relevant event. /// /// The line received from the control connection. private void Dispatch(string line) { if (line == null) throw new ArgumentNullException("line"); string trimmed = line.Trim(); if (trimmed.Length < 3) return; string[] parts = null; if (trimmed.Length > 3 && trimmed[3] == '-') { string[] intermediate = trimmed.Split(new[] { '-' }, 2); // [650, EVENT....] if (intermediate.Length < 2) return; string status = intermediate[0].Trim(); string content = intermediate[1].Trim(); string[] data = content.Split(new string[] { EOL }, 2, StringSplitOptions.None); // [EVENT, ...] if (data.Length < 2) return; parts = new string[] { status, data[0], data[1] }; } else parts = trimmed.Split(new[] { ' ' }, 3); if (parts.Length < 2) return; if (parts[0].Equals("650")) { Event evt = ReflectionHelper.GetEnumerator(attr => parts[1].Equals(attr.Description, StringComparison.CurrentCultureIgnoreCase)); if (evt == Event.Unknown) return; Type type; if (!dispatchers.TryGetValue(evt, out type)) return; Dispatcher dispatcher = Activator.CreateInstance(type) as Dispatcher; dispatcher.Client = client; dispatcher.CurrentEvent = evt; dispatcher.Events = this; if (dispatcher == null) return; dispatcher.Dispatch(parts[2]); } } /// /// Removes an event from the list of monitored events. /// /// The event which should be removed. private void RemoveEvent(Event evt) { if (disposed) throw new ObjectDisposedException("this"); lock (synchronize) { int counter = 0; if (!events.ContainsKey(evt)) return; events.TryGetValue(evt, out counter); counter--; if (counter == 0) { events.Remove(evt); SetEvents(); } else events[evt] = counter; } } /// /// Signals to the control connection to begin monitoring for interested events. /// private void SetEvents() { if (disposed) throw new ObjectDisposedException("this"); lock (synchronize) { if (socket == null || !socket.Connected || !enabled) return; StringBuilder builder = new StringBuilder("SETEVENTS"); foreach (KeyValuePair interested in events) { string name = ReflectionHelper.GetDescription(interested.Key); builder.Append(' '); builder.Append(name); } string command = builder.Append("\r\n").ToString(); byte[] buffer = Encoding.ASCII.GetBytes(command); socket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, OnSocketSendEvents, null); } } /// /// Sets the current response data received from the control port, which is event information. /// /// The response received from the control port. private void SetEventResponse(string response) { if (response == null) throw new ArgumentNullException("response"); if (response.Length == 0) return; if (backlog.Length > 0) response = new StringBuilder(backlog.Length + response.Length).Append(backlog).Append(response).ToString(); int index = 0; int length = response.Length; List dispatch = new List(); if (response.Contains(EOL)) { for (int next = response.IndexOf(EOL); 0 <= next; next = response.IndexOf(EOL, next)) { string line = response.Substring(index, next - index); next += EOL.Length; index = next; if (line.Length == 0) { if (index == length) break; continue; } if (line.StartsWith("250")) continue; if (3 < line.Length && line[3] == '-') { if (multiLine == null) multiLine = new StringBuilder(); multiLine.AppendLine(line); continue; } if (multiLine != null) { dispatch.Add(multiLine.ToString()); multiLine = null; } else dispatch.Add(line); if (index == length) break; } } if (backlog.Length > 0 && index == length) backlog = new StringBuilder(); if (index < length) backlog = new StringBuilder(response.Substring(index)); if (dispatch.Count > 0) foreach (string line in dispatch) Dispatch(line); } /// /// Starts the process of monitoring for events by launching a control connection. /// /// A method raised when the connection has completed. internal void Start(Action callback) { if (disposed) throw new ObjectDisposedException("this"); lock (synchronize) { if (socket != null || !enabled) return; connectCallback = callback; socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, 1); socket.BeginConnect(client.GetClientAddress(), client.GetControlPort(), OnSocketConnect, null); } } /// /// Shuts down the listener socket and releases all resources associated with it. /// internal void Shutdown() { lock (synchronize) { if (socket != null) { try { socket.Shutdown(SocketShutdown.Both); } catch { } socket.Dispose(); socket = null; } } } } }