Documentation and HTTP keep-alive tweak.

feature-npv2
Icedream 2014-05-09 14:33:10 +02:00
parent 6c685d6a96
commit 1297afc0ee
3 changed files with 210 additions and 42 deletions

View File

@ -1,8 +1,10 @@
using System; using System;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Security.Authentication;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using log4net;
namespace NPSharp.Authentication namespace NPSharp.Authentication
{ {
@ -15,6 +17,7 @@ namespace NPSharp.Authentication
private readonly string _host; private readonly string _host;
private readonly string _path; private readonly string _path;
private readonly ushort _port; private readonly ushort _port;
private readonly ILog _log;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="NPSharp.Authentication.SessionAuthenticationClient" /> class. /// Initializes a new instance of the <see cref="NPSharp.Authentication.SessionAuthenticationClient" /> class.
@ -27,6 +30,7 @@ namespace NPSharp.Authentication
_host = host; _host = host;
_port = port; _port = port;
_path = path; _path = path;
_log = LogManager.GetLogger("AuthClient");
} }
/// <summary> /// <summary>
@ -60,9 +64,95 @@ namespace NPSharp.Authentication
/// <param name="password">The password to use for authentication.</param> /// <param name="password">The password to use for authentication.</param>
public void Authenticate(string username, string password) public void Authenticate(string username, string password)
{ {
string post = string.Format("{0}&&{1}", username, password); _log.Debug("Authentication running...");
try
{
AuthenticateUsingNewFormat(username, password);
}
catch (InvalidCredentialException)
{
_log.Debug("Trying login again using old format");
AuthenticateUsingOldFormat(username, password);
}
_log.Debug("Authentication successful.");
}
Uri uri = new UriBuilder protected void AuthenticateUsingNewFormat(string username, string password)
{
var post = string.Format("user={0}&pass={1}", Uri.EscapeDataString(username), Uri.EscapeDataString(password));
var uri = new UriBuilder
{
Scheme = "http",
Port = _port,
Host = _host,
Path = _path
}.Uri;
var req = (HttpWebRequest)WebRequest.Create(uri);
req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded";
req.AllowAutoRedirect = true;
req.KeepAlive = true;
using (var reqStream = req.GetRequestStream())
{
var buffer = Encoding.UTF8.GetBytes(post);
reqStream.Write(buffer, 0, post.Length);
reqStream.Flush();
}
// Response will be in this syntax:
// (ok|fail)#text#userid#username#email#sessiontoken
var rx =
new Regex(
"^(?<status>ok|fail)#(?<text>.+)#(?<userid>[0-9]+)#(?<username>.+)#(?<usermail>.+)#(?<sessiontoken>[^#]+)[#]*$");
var resp = (HttpWebResponse)req.GetResponse();
using (var respStream = resp.GetResponseStream())
{
if (respStream == null)
throw new Exception(@"No answer from server");
using (var respReader = new StreamReader(respStream))
{
while (!respReader.EndOfStream)
{
var line = respReader.ReadLine();
// No answer?
if (string.IsNullOrEmpty(line))
continue;
// DW response line found?
if (!rx.IsMatch(line))
continue;
// This is a DW response line, analyze
var rxm = rx.Match(line);
// Login succeeded?
if (rxm.Groups["status"].Value != "ok")
{
throw new InvalidCredentialException(rxm.Groups["text"].Value);
}
// Store all data
Username = rxm.Groups["username"].Value;
UserEMail = rxm.Groups["usermail"].Value;
SessionToken = rxm.Groups["sessiontoken"].Value;
UserId = uint.Parse(rxm.Groups["userid"].Value);
return;
}
}
}
throw new Exception("This is not an authentication server");
}
protected void AuthenticateUsingOldFormat(string username, string password)
{
var post = string.Format("{0}&&{1}", username, password);
var uri = new UriBuilder
{ {
Scheme = "http", Scheme = "http",
Port = _port, Port = _port,
@ -75,9 +165,9 @@ namespace NPSharp.Authentication
req.ContentType = "application/x-www-form-urlencoded"; req.ContentType = "application/x-www-form-urlencoded";
req.AllowAutoRedirect = true; req.AllowAutoRedirect = true;
req.KeepAlive = false; req.KeepAlive = false;
using (Stream reqStream = req.GetRequestStream()) using (var reqStream = req.GetRequestStream())
{ {
byte[] buffer = Encoding.UTF8.GetBytes(post); var buffer = Encoding.UTF8.GetBytes(post);
reqStream.Write(buffer, 0, post.Length); reqStream.Write(buffer, 0, post.Length);
reqStream.Flush(); reqStream.Flush();
} }
@ -88,7 +178,7 @@ namespace NPSharp.Authentication
new Regex( new Regex(
"^(?<status>ok|fail)#(?<text>.+)#(?<userid>[0-9]+)#(?<username>.+)#(?<usermail>.+)#(?<sessiontoken>[^#]+)[#]*$"); "^(?<status>ok|fail)#(?<text>.+)#(?<userid>[0-9]+)#(?<username>.+)#(?<usermail>.+)#(?<sessiontoken>[^#]+)[#]*$");
var resp = (HttpWebResponse)req.GetResponse(); var resp = (HttpWebResponse)req.GetResponse();
using (Stream respStream = resp.GetResponseStream()) using (var respStream = resp.GetResponseStream())
{ {
if (respStream == null) if (respStream == null)
throw new Exception(@"No answer from server"); throw new Exception(@"No answer from server");
@ -96,7 +186,7 @@ namespace NPSharp.Authentication
{ {
while (!respReader.EndOfStream) while (!respReader.EndOfStream)
{ {
string line = respReader.ReadLine(); var line = respReader.ReadLine();
// No answer? // No answer?
if (string.IsNullOrEmpty(line)) if (string.IsNullOrEmpty(line))
@ -107,20 +197,26 @@ namespace NPSharp.Authentication
continue; continue;
// This is a DW response line, analyze // This is a DW response line, analyze
Match rxm = rx.Match(line); var rxm = rx.Match(line);
// Login succeeded? // Login succeeded?
if (rxm.Groups["status"].Value != "ok") if (rxm.Groups["status"].Value != "ok")
throw new Exception(rxm.Groups["text"].Value); {
throw new InvalidCredentialException(rxm.Groups["text"].Value);
}
// Store all data // Store all data
Username = rxm.Groups["username"].Value; Username = rxm.Groups["username"].Value;
UserEMail = rxm.Groups["usermail"].Value; UserEMail = rxm.Groups["usermail"].Value;
SessionToken = rxm.Groups["sessiontoken"].Value; SessionToken = rxm.Groups["sessiontoken"].Value;
UserId = uint.Parse(rxm.Groups["userid"].Value); UserId = uint.Parse(rxm.Groups["userid"].Value);
return;
} }
} }
} }
throw new Exception("This is not an authentication server");
} }
} }
} }

View File

@ -12,28 +12,45 @@ using uhttpsharp.RequestProviders;
namespace NPSharp.Authentication namespace NPSharp.Authentication
{ {
/// <summary>
/// Represents a session authentication server which uses the HTTP protocol to send out session tokens to authenticating NP clients.
/// </summary>
public class SessionAuthenticationServer public class SessionAuthenticationServer
{ {
private readonly ILog _log;
private HttpServer _http; private HttpServer _http;
private readonly ILog _log; /// <summary>
/// Constructs a new session authentication server.
/// </summary>
public SessionAuthenticationServer() public SessionAuthenticationServer()
{ {
SupportOldAuthentication = true; SupportOldAuthentication = true;
_log = LogManager.GetLogger("Auth"); _log = LogManager.GetLogger("Auth");
} }
/// <summary>
/// Support oldskool "user&amp;&amp;pass" authentication format.
/// </summary>
public bool SupportOldAuthentication { get; set; }
/// <summary>
/// Will be triggered whenever a client tries to authenticate via this server.
/// </summary>
public event Func<string, string, SessionAuthenticationResult> Authenticating; public event Func<string, string, SessionAuthenticationResult> Authenticating;
protected virtual SessionAuthenticationResult OnAuthenticating(string username, string password) protected virtual SessionAuthenticationResult OnAuthenticating(string username, string password)
{ {
var handler = Authenticating; var handler = Authenticating;
return handler != null ? handler(username, password) : new SessionAuthenticationResult { Reason = "Login currently disabled" }; return handler != null
? handler(username, password)
: new SessionAuthenticationResult {Reason = "Login currently disabled"};
} }
public bool SupportOldAuthentication { get; set; } /// <summary>
/// Starts the authentication server.
/// </summary>
/// <param name="port">The port on which the authentication server should listen on.</param>
public void Start(ushort port = 12003) public void Start(ushort port = 12003)
{ {
if (_http != null) if (_http != null)
@ -45,12 +62,21 @@ namespace NPSharp.Authentication
_http.Use(new HttpRouter().With("authenticate", new AuthenticateHandler(this))); _http.Use(new HttpRouter().With("authenticate", new AuthenticateHandler(this)));
_http.Use(new AnonymousHttpRequestHandler((ctx, task) => _http.Use(new AnonymousHttpRequestHandler((ctx, task) =>
{ {
ctx.Response = HttpResponse.CreateWithMessage(HttpResponseCode.NotFound, "Not found", ctx.Request.Headers.KeepAliveConnection()); ctx.Response = HttpResponse.CreateWithMessage(HttpResponseCode.NotFound, "Not found",
ctx.Request.Headers.KeepAliveConnection());
return Task.Factory.GetCompleted(); return Task.Factory.GetCompleted();
})); }));
_http.Start(); _http.Start();
} }
/// <summary>
/// Stops the authentication server.
/// </summary>
public void Stop()
{
_http.Dispose();
}
protected class AuthenticateHandler : IHttpRequestHandler protected class AuthenticateHandler : IHttpRequestHandler
{ {
private readonly SessionAuthenticationServer _authServer; private readonly SessionAuthenticationServer _authServer;
@ -64,44 +90,79 @@ namespace NPSharp.Authentication
{ {
SessionAuthenticationResult sar; SessionAuthenticationResult sar;
string username;
string password;
if (!context.Request.Post.Parsed.TryGetByName("user", out username) ||
!context.Request.Post.Parsed.TryGetByName("pass", out password))
{
var login = Encoding.UTF8.GetString(context.Request.Post.Raw) var login = Encoding.UTF8.GetString(context.Request.Post.Raw)
.Split(new[] {"&&"}, StringSplitOptions.None); .Split(new[] {"&&"}, StringSplitOptions.None);
if (login.Length != 2) if (login.Length != 2)
{ {
sar = new SessionAuthenticationResult {Reason = @"Invalid login data"}; sar = new SessionAuthenticationResult {Reason = @"Invalid login data"};
context.Response = new HttpResponse(HttpResponseCode.Ok, sar.ToString(),
context.Request.Headers.KeepAliveConnection());
return Task.Factory.GetCompleted();
} }
else
{ username = login[0];
password = login[1];
}
try try
{ {
sar = _authServer.OnAuthenticating(login[0], login[1]); sar = _authServer.OnAuthenticating(username, password);
} }
catch (Exception error) catch (Exception error)
{ {
_authServer._log.Error(@"Authentication handler error", error); _authServer._log.Error(@"Authentication handler error", error);
sar = new SessionAuthenticationResult {Reason = @"Internal server error"}; sar = new SessionAuthenticationResult {Reason = @"Internal server error"};
} }
}
context.Response = new HttpResponse(HttpResponseCode.Ok, sar.ToString(), context.Request.Headers.KeepAliveConnection()); context.Response = new HttpResponse(HttpResponseCode.Ok, sar.ToString(),
!sar.Success && context.Request.Headers.KeepAliveConnection());
return Task.Factory.GetCompleted(); return Task.Factory.GetCompleted();
} }
} }
public void Stop()
{
_http.Dispose();
}
} }
public class SessionAuthenticationResult public class SessionAuthenticationResult
{ {
/// <summary>
/// true if authentication was successful, otherwise false.
/// </summary>
public bool Success { get; set; } public bool Success { get; set; }
/// <summary>
/// Reason for the given success state. Use this especially in authentication fail cases.
/// </summary>
public string Reason { get; set; } public string Reason { get; set; }
/// <summary>
/// If authenticated set this to the user's unique ID.
/// </summary>
public uint UserID { get; set; } public uint UserID { get; set; }
/// <summary>
/// If authenticated set this to the user's session token.
/// </summary>
public string SessionToken { get; set; } public string SessionToken { get; set; }
/// <summary>
/// If authenticated set this to the actual correctly spelled username.
/// </summary>
public string UserName { get; set; } public string UserName { get; set; }
/// <summary>
/// If authenticated set this to the user's e-mail address.
/// </summary>
public string UserMail { get; set; } public string UserMail { get; set; }
/// <summary>
/// Returns the response line as it should be sent out to the client.
/// </summary>
public override string ToString() public override string ToString()
{ {
return String.Join("#", return String.Join("#",

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using log4net; using log4net;
@ -196,7 +195,7 @@ namespace NPSharp
client.UserID = result.UserID; client.UserID = result.UserID;
// Send "online" notification to all friends of this player // Send "online" notification to all friends of this player
foreach (NPServerClient fconn in client.FriendConnections) foreach (var fconn in client.FriendConnections)
{ {
fconn.RPC.Send(new FriendsPresenceMessage fconn.RPC.Send(new FriendsPresenceMessage
{ {
@ -207,6 +206,12 @@ namespace NPSharp
}); });
} }
// Send friends roster to player
client.RPC.Send(new FriendsRosterMessage
{
Friends = client.Friends.ToArray()
});
OnClientAuthenticated(client); OnClientAuthenticated(client);
}); });
@ -244,7 +249,7 @@ namespace NPSharp
client.UserID = result.UserID; client.UserID = result.UserID;
// Send "online" notification to all friends of this player // Send "online" notification to all friends of this player
foreach (NPServerClient fconn in client.FriendConnections) foreach (var fconn in client.FriendConnections)
{ {
fconn.RPC.Send(new FriendsPresenceMessage fconn.RPC.Send(new FriendsPresenceMessage
{ {
@ -255,6 +260,12 @@ namespace NPSharp
}); });
} }
// Send friends roster to player
client.RPC.Send(new FriendsRosterMessage
{
Friends = client.Friends.ToArray()
});
OnClientAuthenticated(client); OnClientAuthenticated(client);
}); });
@ -331,7 +342,7 @@ namespace NPSharp
client.RPC.AttachHandlerForMessageType<FriendsSetPresenceMessage>(msg => client.RPC.AttachHandlerForMessageType<FriendsSetPresenceMessage>(msg =>
{ {
foreach (FriendsPresence pdata in msg.Presence) foreach (var pdata in msg.Presence)
{ {
client.SetPresence(pdata.Key, pdata.Value); client.SetPresence(pdata.Key, pdata.Value);
_log.DebugFormat("Client says presence \"{0}\" is \"{1}\"", pdata.Key, pdata.Value); _log.DebugFormat("Client says presence \"{0}\" is \"{1}\"", pdata.Key, pdata.Value);