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();
}
}
}
}