using PleaseIgnore.IntelMap.Properties; using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; using System.Net; using System.Security.Authentication; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; namespace PleaseIgnore.IntelMap { /// /// Provides low-level access to the reporting features of the Test /// Alliance Intel Map. /// /// public class IntelSession : IDisposable { // The Unix time epoc private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); // Field separators for the channel list private static readonly char[] ChannelSeparators = new char[] { ',' }; // API response parsers private static readonly Regex ErrorResponse = new Regex(@"^(50\d) ERROR (.*)"); private static readonly Regex AuthResponse = new Regex(@"^200 AUTH ([^\s]+) (\d+)"); private static readonly Regex IntelResponse = new Regex(@"^202 INTEL .*"); private static readonly Regex AliveResponse = new Regex(@"^203 ALIVE OK (\d+)"); // Number of server errors before dropping the connection private const int maxServerErrors = 3; // The username for this login (required when logging out) private readonly string username; // The session id private readonly string session; // The service access Uri private readonly Uri serviceUri; // Number of consecutive server errors private int serverErrors; /// /// Creates a new instance of the class /// and authenticates with the map server. /// /// /// The user's AUTH name. /// /// /// An SHA1 hash of the user's password. /// /// /// The authentication failed. /// /// /// Unexpected response returned from the server. /// /// /// Failed to contact the web server. /// /// public IntelSession(string username, string passwordHash) : this(username, passwordHash, null) { } /// /// Creates a new instance of the class /// and authenticates with the map server using a specified service /// . /// /// /// The user's AUTH name. /// /// /// An SHA1 hash of the user's password. /// /// /// to use when contacting the Intel Map reporting /// service. /// /// /// is primarily intended /// for use with unit testing. Normal users will make use of the /// implementation. /// /// /// The authentication failed. /// /// /// Unexpected response returned from the server. /// /// /// Failed to contact the web server. /// /// /// uses a URI scheme not supported /// by . /// /// public IntelSession(string username, string passwordHash, Uri serviceUri) { Contract.Requires(!String.IsNullOrEmpty(username)); Contract.Requires(!String.IsNullOrEmpty(passwordHash)); Contract.Requires((serviceUri == null) || serviceUri.IsAbsoluteUri); this.username = username; this.serviceUri = serviceUri ?? IntelExtensions.ReportUrl; var request = WebRequest.Create(this.serviceUri); var response = request.Post(new Dictionary() { { "username", username }, { "password", passwordHash }, { "action", "AUTH" }, { "version", "2.2.0" } }); var responseBody = response.ReadContent(); Match match; if ((match = AuthResponse.Match(responseBody)).Success) { // Successfully authenticated this.session = match.Groups[1].Value; this.Users = match.Groups[2].ToInt32(); this.IsConnected = true; } else if ((match = ErrorResponse.Match(responseBody)).Success) { // Authentication failed throw new AuthenticationException(match.Groups[2].Value); } else { // The server responded with something unexpected throw new WebException(Resources.IntelException, WebExceptionStatus.ProtocolError); } } ~IntelSession() { this.Dispose(false); } /// /// Gets a flag indicating whether our session with the intel /// reporting server is still valid. /// public bool IsConnected { get; private set; } /// /// Gets the number of users currently connected to the server. /// public int Users { get; private set; } /// /// Gets the number of intel reports sent to the server. /// public int ReportsSent { get; private set; } /// /// Occurs when this session with the server is closed, either /// through a call to or timing out. /// public event EventHandler Closed; /// /// Sends a keep-alive to the intel reporting server, preserving /// our session. /// /// /// if our session is still valid; /// otherwise, . /// /// /// Unexpected response returned from the server. /// /// /// Failed to contact the web server. /// public virtual bool KeepAlive() { Contract.Ensures(Contract.Result() == this.IsConnected); if (!this.IsConnected) return false; try { var request = WebRequest.Create(this.serviceUri); var response = request.Post(new Dictionary() { { "session", this.session }, { "action", "ALIVE" }, }); var responseBody = response.ReadContent(); Match match; if ((match = AliveResponse.Match(responseBody)).Success) { // Successful ping of the server this.Users = match.Groups[1].ToInt32(); return true; } else if ((match = ErrorResponse.Match(responseBody)).Success) { if (match.Groups[1].Value == "502") { // Our session has expired this.OnClosed(); return false; } else { // The server responded with something unexpected throw new WebException(Resources.IntelException, WebExceptionStatus.ProtocolError); } } else { // The server responded with something unexpected throw new WebException(Resources.IntelException, WebExceptionStatus.ProtocolError); } } catch { this.OnError(); throw; } } /// /// Sends a log entry to the intel reporting server. /// /// /// if our session is still valid; /// otherwise, . /// /// /// Unexpected response returned from the server. /// /// /// Failed to contact the web server. /// public virtual bool Report(string channel, DateTime timestamp, string message) { Contract.Requires(!String.IsNullOrEmpty(channel)); Contract.Requires(!String.IsNullOrEmpty(message)); Contract.Ensures(Contract.Result() == this.IsConnected); if (!this.IsConnected) return false; try { var request = WebRequest.Create(this.serviceUri); var response = request.Post(new Dictionary() { { "session", session }, { "inteltime", timestamp.ToUnixTime() .ToString("F0", CultureInfo.InvariantCulture) }, { "action", "INTEL" }, { "region", channel }, // XXX: The \r is to make our report match the perl version EXACTLY { "intel", message + '\r' } }); var responseBody = response.ReadContent(); Match match; if ((match = IntelResponse.Match(responseBody)).Success) { // Successfully reported intel ++this.ReportsSent; this.serverErrors = 0; return true; } else if ((match = ErrorResponse.Match(responseBody)).Success) { if (match.Groups[1].Value == "502") { // Our session has expired this.OnClosed(); return false; } else { // The server responded with something unexpected throw new WebException(Resources.IntelException, WebExceptionStatus.ProtocolError); } } else { // The server responded with something unexpected throw new WebException(Resources.IntelException, WebExceptionStatus.ProtocolError); } } catch { this.OnError(); throw; } } /// /// Sends a log entry to the intel reporting server. /// /// /// An instance of containing the /// information to report. /// /// /// if our session is still valid; /// otherwise, . /// public bool Report(IntelEventArgs e) { Contract.Requires(e != null, "e"); return this.Report(e.Channel, e.Timestamp, e.Message); } /// /// Closes this session with the intel reporting server. /// public void Dispose() { Contract.Ensures(this.IsConnected == false); this.Dispose(true); GC.SuppressFinalize(this); } /// /// Closes this session with the intel reporting server. /// protected virtual void Dispose(bool disposing) { Contract.Ensures(this.IsConnected == false); if (disposing) { if (!this.IsConnected) return; try { var request = WebRequest.Create(this.serviceUri); var response = request.Post(new Dictionary() { { "username", this.username }, { "session", this.session }, { "action", "LOGOFF" }, }); // ReadContent() handles tracing the response var responseBody = response.ReadContent(); } catch (WebException) { // We don't actually care... } finally { this.OnClosed(); } } else { this.IsConnected = false; } } /// public override string ToString() { return String.Format( CultureInfo.CurrentCulture, this.IsConnected ? Properties.Resources.IntelSession_Connected : Properties.Resources.IntelSession_Disposed, this.GetType().Name, this.Users, this.ReportsSent); } /// /// Signals that an error has occured contacting the server. /// /// /// /// private void OnError() { if (++this.serverErrors == maxServerErrors) { this.OnClosed(); } } /// /// Raises the event when the session is /// closed. /// private void OnClosed() { Contract.Ensures(!this.IsConnected); this.IsConnected = false; this.Users = 0; var handler = this.Closed; this.Closed = null; if (handler != null) { handler(this, EventArgs.Empty); } } [ContractInvariantMethod] private void ObjectInvariant() { Contract.Invariant(this.Users >= 0); Contract.Invariant(this.ReportsSent >= 0); Contract.Invariant(!this.IsConnected || (this.serverErrors < maxServerErrors)); } /// /// Hashes a user's AUTH password in the manner required by /// authentication with the intel map reporting server. /// /// /// The plain text password to be hashed. /// /// /// The hashed representation of . /// [Pure] public static string HashPassword(string password) { Contract.Requires(!String.IsNullOrEmpty(password)); Contract.Ensures(Contract.Result() != null); Contract.Ensures(Contract.Result().Length == 40); using (var sha = SHA1.Create()) { return sha.ComputeHash(Encoding.UTF8.GetBytes(password)).ToLowerHexString(); } } } }