diff --git a/src/libnpsharp/NPClient.cs b/src/libnpsharp/NPClient.cs new file mode 100644 index 0000000..c61698a --- /dev/null +++ b/src/libnpsharp/NPClient.cs @@ -0,0 +1,221 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using log4net; +using NPSharp.RPC; +using NPSharp.RPC.Packets; + +namespace NPSharp +{ + /// + /// Represents a high-level network platform client. + /// + public class NPClient + { + private readonly RPCClientStream _rpc; + private CancellationTokenSource _cancellationTokenSource; + private CancellationToken _cancellationToken; + private ILog _log; + + /// + /// Initializes the NP client with a specified host and port. + /// + /// The host to connect to. + /// The port to use. Default: 3025. + public NPClient(string host, ushort port = 3025) + { + _rpc = new RPCClientStream(host, port); + _log = LogManager.GetLogger ("NPClient"); + } + + /// + /// The assigned NP user ID. Will be set on successful authentication. + /// + public ulong LoginId { get; private set; } + + /// + /// The assigned session token for this client. Will be set on successful authentication. + /// + public string SessionToken { get; private set; } + + // TODO: Handle connection failures via exception + /// + /// Connects the client to the NP server. + /// + /// True if the connection succeeded, otherwise false. + public bool Connect() + { + _cancellationTokenSource = new CancellationTokenSource(); + _cancellationToken = _cancellationTokenSource.Token; + + if (!_rpc.Open()) + return false; + + Task.Factory.StartNew(() => + { + _log.Debug("Now receiving RPC messages"); + try + { + while (true) + { + var message = _rpc.Read(); + if (message == null) + continue; + + // TODO: log4net + Console.WriteLine("Received packet ID {1} (type {0})", message.GetType().Name, message.MessageId); + } + } + catch (ProtocolViolationException error) + { + _log.ErrorFormat("Protocol violation: {0}. Disconnect imminent.", error.Message); + Disconnect(); + } + + _log.Debug("Now not receiving RPC messages anymore"); + }, _cancellationToken); + + return true; + } + + /// + /// Disconnects the client from the NP server. + /// + public void Disconnect() + { + _cancellationTokenSource.Cancel(true); // TODO: Find a cleaner way to cancel _processingTask (focus: _rpc.Read) + _rpc.Close(); + + LoginId = 0; + } + + // TODO: Try to use an exception for failed action instead + /// + /// Authenticates this connection via a token. This token has to be requested via an external interface like remauth.php. + /// + /// The token to use for authentication + /// True if the login succeeded, otherwise false. + public async Task AuthenticateWithToken(string token) + { + var tcs = new TaskCompletionSource(); + + _rpc.AttachCallback(packet => + { + var result = (AuthenticateResultMessage) packet; + if (result.Result != 0) + tcs.SetResult(false); + LoginId = result.NPID; + SessionToken = result.SessionToken; + tcs.SetResult(true); + }); + _rpc.Send(new AuthenticateWithTokenMessage {Token = token}); + + return await tcs.Task; + } + + // TODO: Try to use an exception for failed action instead + /// + /// Uploads a user file. + /// + /// The file name to save the contents to on the server + /// The raw byte contents + /// True if the upload succeeded, otherwise false. + public async Task UploadUserFile(string filename, byte[] contents) + { + var tcs = new TaskCompletionSource(); + + _rpc.AttachCallback(packet => + { + var result = (StorageWriteUserFileResultMessage) packet; + if (result.Result != 0) + tcs.SetResult(false); + tcs.SetResult(true); + }); + _rpc.Send(new StorageWriteUserFileMessage {FileData = contents, FileName = filename, NPID = LoginId}); + + return await tcs.Task; + } + + /// + /// Downloads a user file and returns its contents. + /// + /// The file to download + /// File contents as byte array + public async Task GetUserFile(string filename) + { + var tcs = new TaskCompletionSource(); + + _rpc.AttachCallback(packet => + { + var result = (StorageUserFileMessage) packet; + if (result.Result != 0) + { + tcs.SetException(new NpFileException()); + return; + } + tcs.SetResult(result.FileData); + }); + _rpc.Send(new StorageGetUserFileMessage {FileName = filename, NPID = LoginId}); + + return await tcs.Task; + } + + + /// + /// Downloads a user file onto the harddisk. + /// + /// The file to download + /// Path where to save the file + public async void DownloadUserFileTo(string filename, string targetpath) + { + var contents = await GetUserFile(filename); + + File.WriteAllBytes(targetpath, contents); + } + + + /// + /// Downloads a publisher file and returns its contents. + /// + /// The file to download + /// File contents as byte array + public async Task GetPublisherFile(string filename) + { + var tcs = new TaskCompletionSource(); + + _rpc.AttachCallback(packet => + { + var result = (StoragePublisherFileMessage) packet; + if (result.Result != 0) + { + tcs.SetException(new NpFileException()); + return; + } + tcs.SetResult(result.FileData); + }); + _rpc.Send(new StorageGetPublisherFileMessage {FileName = filename}); + + return await tcs.Task; + } + + /// + /// Downloads a publisher file onto the harddisk. + /// + /// The file to download + /// Path where to save the file + public async void DownloadPublisherFileTo(string filename, string targetpath) + { + var contents = await GetPublisherFile(filename); + + File.WriteAllBytes(targetpath, contents); + } + + public void SendRandomString(string data) + { + _rpc.Send(new StorageSendRandomStringMessage() { RandomString=data }); + } + } +} \ No newline at end of file diff --git a/src/libnpsharp/RPC/Packets/AuthenticateExternalStatusMessage.cs b/src/libnpsharp/RPC/Packets/AuthenticateExternalStatusMessage.cs new file mode 100644 index 0000000..78f2f56 --- /dev/null +++ b/src/libnpsharp/RPC/Packets/AuthenticateExternalStatusMessage.cs @@ -0,0 +1,12 @@ +using ProtoBuf; + +namespace NPSharp.RPC.Packets +{ + [Packet(1006)] + [ProtoContract] + class AuthenticateExternalStatusMessage : RPCServerMessage + { + [ProtoMember(1)] + public int Status { get; set; } + } +} \ No newline at end of file diff --git a/src/libnpsharp/RPC/Packets/AuthenticateResultMessage.cs b/src/libnpsharp/RPC/Packets/AuthenticateResultMessage.cs new file mode 100644 index 0000000..42cb81c --- /dev/null +++ b/src/libnpsharp/RPC/Packets/AuthenticateResultMessage.cs @@ -0,0 +1,18 @@ +using ProtoBuf; + +namespace NPSharp.RPC.Packets +{ + [Packet(1010)] + [ProtoContract] + class AuthenticateResultMessage : RPCServerMessage + { + [ProtoMember(1)] + public int Result { get; set; } + + [ProtoMember(2)] + public ulong NPID { get; set; } + + [ProtoMember(3)] + public string SessionToken { get; set; } + } +} \ No newline at end of file diff --git a/src/libnpsharp/RPC/Packets/AuthenticateWithTokenMessage.cs b/src/libnpsharp/RPC/Packets/AuthenticateWithTokenMessage.cs new file mode 100644 index 0000000..eff3199 --- /dev/null +++ b/src/libnpsharp/RPC/Packets/AuthenticateWithTokenMessage.cs @@ -0,0 +1,12 @@ +using ProtoBuf; + +namespace NPSharp.RPC.Packets +{ + [Packet(1003)] + [ProtoContract] + class AuthenticateWithTokenMessage : RPCClientMessage + { + [ProtoMember(1)] + public string Token { get; set; } + } +} diff --git a/src/libnpsharp/RPC/Packets/CloseAppMessage.cs b/src/libnpsharp/RPC/Packets/CloseAppMessage.cs new file mode 100644 index 0000000..2dd2710 --- /dev/null +++ b/src/libnpsharp/RPC/Packets/CloseAppMessage.cs @@ -0,0 +1,12 @@ +using ProtoBuf; + +namespace NPSharp.RPC.Packets +{ + [Packet(2001)] + [ProtoContract] + class CloseAppMessage : RPCServerMessage + { + [ProtoMember(1)] + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/src/libnpsharp/RPC/Packets/HelloMessage.cs b/src/libnpsharp/RPC/Packets/HelloMessage.cs new file mode 100644 index 0000000..42cb333 --- /dev/null +++ b/src/libnpsharp/RPC/Packets/HelloMessage.cs @@ -0,0 +1,22 @@ +using ProtoBuf; + +namespace NPSharp.RPC.Packets +{ + [Packet(1000)] + [ProtoContract] + class HelloMessage : RPCServerMessage + { + // I seriously have no idea where in the code this is used but whatever + [ProtoMember(1)] + public int Number1 { get; set; } + + [ProtoMember(2)] + public int Number2 { get; set; } + + [ProtoMember(3)] + public string Name { get; set; } + + [ProtoMember(4)] + public string String2 { get; set; } + } +} diff --git a/src/libnpsharp/RPC/Packets/MessagingSendDataMessage.cs b/src/libnpsharp/RPC/Packets/MessagingSendDataMessage.cs new file mode 100644 index 0000000..360787c --- /dev/null +++ b/src/libnpsharp/RPC/Packets/MessagingSendDataMessage.cs @@ -0,0 +1,15 @@ +using ProtoBuf; + +namespace NPSharp.RPC.Packets +{ + [Packet(2002)] + [ProtoContract] + class MessagingSendDataMessage : RPCClientMessage + { + [ProtoMember(1)] + public ulong NPID { get; set; } + + [ProtoMember(2)] + public byte[] Data { get; set; } + } +} \ No newline at end of file diff --git a/src/libnpsharp/RPC/Packets/PacketAttribute.cs b/src/libnpsharp/RPC/Packets/PacketAttribute.cs new file mode 100644 index 0000000..ed5124b --- /dev/null +++ b/src/libnpsharp/RPC/Packets/PacketAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace NPSharp.RPC.Packets +{ + class PacketAttribute : Attribute + { + public PacketAttribute(uint type) + { + Type = type; + } + + public uint Type { get; set; } + } +} diff --git a/src/libnpsharp/RPC/Packets/RPCClientMessage.cs b/src/libnpsharp/RPC/Packets/RPCClientMessage.cs new file mode 100644 index 0000000..d08ae6f --- /dev/null +++ b/src/libnpsharp/RPC/Packets/RPCClientMessage.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; + +namespace NPSharp.RPC.Packets +{ + public class RPCClientMessage : RPCMessage + { + public byte[] Serialize(uint id) + { + byte[] content; + using (var bufferStream = new MemoryStream()) + { + ProtoBuf.Serializer.Serialize(bufferStream, this); + bufferStream.Seek(0, SeekOrigin.Begin); + content = bufferStream.ToArray(); + } + + var buffer = new List(); + buffer.AddRange(BitConverter.GetBytes((uint)IPAddress.HostToNetworkOrder(Signature))); + buffer.AddRange(BitConverter.GetBytes((uint)IPAddress.HostToNetworkOrder(content.Length))); + buffer.AddRange(BitConverter.GetBytes((uint)IPAddress.HostToNetworkOrder(GetTypeId()))); + buffer.AddRange(BitConverter.GetBytes((uint)IPAddress.HostToNetworkOrder(id))); + buffer.AddRange(content); + + return buffer.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/libnpsharp/RPC/Packets/RPCMessage.cs b/src/libnpsharp/RPC/Packets/RPCMessage.cs new file mode 100644 index 0000000..7712425 --- /dev/null +++ b/src/libnpsharp/RPC/Packets/RPCMessage.cs @@ -0,0 +1,15 @@ +using System.Linq; + +namespace NPSharp.RPC.Packets +{ + public class RPCMessage + { + internal const uint Signature = 0xDEADC0DE; // I wonder if aiw3 changed this since kernal noted it in his source code. + + public uint GetTypeId() + { + var packet = (PacketAttribute) GetType().GetCustomAttributes(typeof (PacketAttribute), false).Single(); + return packet.Type; + } + } +} diff --git a/src/libnpsharp/RPC/Packets/RPCServerMessage.cs b/src/libnpsharp/RPC/Packets/RPCServerMessage.cs new file mode 100644 index 0000000..84de050 --- /dev/null +++ b/src/libnpsharp/RPC/Packets/RPCServerMessage.cs @@ -0,0 +1,67 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; + +namespace NPSharp.RPC.Packets +{ + public class RPCServerMessage : RPCMessage + { + public uint MessageId { get; private set; } + + public static RPCServerMessage Deserialize(NetworkStream ns) + { + var header = new byte[16]; + var l = ns.Read(header, 0, header.Length); + if (l == 0) + return null; + if (l < 16) + throw new ProtocolViolationException("Received incomplete header"); + + var signature = (uint)IPAddress.NetworkToHostOrder(BitConverter.ToUInt32(header, 0)); + var length = (uint)IPAddress.NetworkToHostOrder(BitConverter.ToUInt32(header, 4)); + var type = (uint)IPAddress.NetworkToHostOrder(BitConverter.ToUInt32(header, 8)); + var buffer = new byte[length]; + ns.Read(buffer, 0, buffer.Length); + + if (signature != Signature) + throw new ProtocolViolationException("Received packet with invalid signature"); + + RPCServerMessage packet; + + using (var ms = new MemoryStream(buffer)) + { + var types = Assembly.GetExecutingAssembly().GetTypes().Where( + t => + t.IsSubclassOf(typeof (RPCServerMessage)) + && + ((PacketAttribute) t.GetCustomAttributes(typeof (PacketAttribute), false).Single()).Type == type + ).ToArray(); + if (!types.Any()) + { + throw new ProtocolViolationException("Received packet of unknown type"); + } + if (types.Count() > 1) + { +#if DEBUG + Debug.Fail(string.Format("Bug in program code: Found more than 1 type for packet ID {0}", type)); +#else + // TODO: log4net + return null; +#endif + } + packet = (RPCServerMessage)ProtoBuf.Serializer.NonGeneric.Deserialize( + types.Single(), + ms + ); + } + + packet.MessageId = (uint)IPAddress.NetworkToHostOrder(BitConverter.ToUInt32(header, 12)); + + return packet; + } + } +} \ No newline at end of file diff --git a/src/libnpsharp/RPC/Packets/StorageGetPublisherFileMessage.cs b/src/libnpsharp/RPC/Packets/StorageGetPublisherFileMessage.cs new file mode 100644 index 0000000..86b6732 --- /dev/null +++ b/src/libnpsharp/RPC/Packets/StorageGetPublisherFileMessage.cs @@ -0,0 +1,12 @@ +using ProtoBuf; + +namespace NPSharp.RPC.Packets +{ + [ProtoContract] + [Packet(1101)] + class StorageGetPublisherFileMessage : RPCClientMessage + { + [ProtoMember(1)] + public string FileName { get; set; } + } +} diff --git a/src/libnpsharp/RPC/Packets/StorageGetUserFileMessage.cs b/src/libnpsharp/RPC/Packets/StorageGetUserFileMessage.cs new file mode 100644 index 0000000..0b27e3b --- /dev/null +++ b/src/libnpsharp/RPC/Packets/StorageGetUserFileMessage.cs @@ -0,0 +1,15 @@ +using ProtoBuf; + +namespace NPSharp.RPC.Packets +{ + [Packet(1102)] + [ProtoContract] + class StorageGetUserFileMessage : RPCClientMessage + { + [ProtoMember(1)] + public string FileName { get; set; } + + [ProtoMember(2)] + public ulong NPID { get; set; } // SERIOUSLY WHY IS THIS EVEN HERE + } +} \ No newline at end of file diff --git a/src/libnpsharp/RPC/Packets/StoragePublisherFileMessage.cs b/src/libnpsharp/RPC/Packets/StoragePublisherFileMessage.cs new file mode 100644 index 0000000..77881b2 --- /dev/null +++ b/src/libnpsharp/RPC/Packets/StoragePublisherFileMessage.cs @@ -0,0 +1,18 @@ +using ProtoBuf; + +namespace NPSharp.RPC.Packets +{ + [Packet(1111)] + [ProtoContract] + class StoragePublisherFileMessage : RPCServerMessage + { + [ProtoMember(1)] + public int Result { get; set; } + + [ProtoMember(2)] + public string FileName { get; set; } + + [ProtoMember(3)] + public byte[] FileData { get; set; } + } +} \ No newline at end of file diff --git a/src/libnpsharp/RPC/Packets/StorageSendRandomStringMessage.cs b/src/libnpsharp/RPC/Packets/StorageSendRandomStringMessage.cs new file mode 100644 index 0000000..6943bf7 --- /dev/null +++ b/src/libnpsharp/RPC/Packets/StorageSendRandomStringMessage.cs @@ -0,0 +1,12 @@ +using ProtoBuf; + +namespace NPSharp.RPC.Packets +{ + [Packet(1104)] + [ProtoContract] + class StorageSendRandomStringMessage : RPCClientMessage + { + [ProtoMember(1)] + public string RandomString { get; set; } + } +} \ No newline at end of file diff --git a/src/libnpsharp/RPC/Packets/StorageUserFileMessage.cs b/src/libnpsharp/RPC/Packets/StorageUserFileMessage.cs new file mode 100644 index 0000000..3a2b85a --- /dev/null +++ b/src/libnpsharp/RPC/Packets/StorageUserFileMessage.cs @@ -0,0 +1,21 @@ +using ProtoBuf; + +namespace NPSharp.RPC.Packets +{ + [Packet(1112)] + [ProtoContract] + class StorageUserFileMessage : RPCServerMessage + { + [ProtoMember(1)] + public int Result { get; set; } + + [ProtoMember(2)] + public string FileName { get; set; } + + [ProtoMember(3)] + public ulong NPID { get; set; } + + [ProtoMember(4)] + public byte[] FileData { get; set; } + } +} \ No newline at end of file diff --git a/src/libnpsharp/RPC/Packets/StorageWriteUserFileMessage.cs b/src/libnpsharp/RPC/Packets/StorageWriteUserFileMessage.cs new file mode 100644 index 0000000..b9a9dd9 --- /dev/null +++ b/src/libnpsharp/RPC/Packets/StorageWriteUserFileMessage.cs @@ -0,0 +1,18 @@ +using ProtoBuf; + +namespace NPSharp.RPC.Packets +{ + [Packet(1103)] + [ProtoContract] + class StorageWriteUserFileMessage : RPCClientMessage + { + [ProtoMember(1)] + public string FileName { get; set; } + + [ProtoMember(2)] + public ulong NPID { get; set; } + + [ProtoMember(3)] + public byte[] FileData { get; set; } + } +} \ No newline at end of file diff --git a/src/libnpsharp/RPC/Packets/StorageWriteUserFileResultMessage.cs b/src/libnpsharp/RPC/Packets/StorageWriteUserFileResultMessage.cs new file mode 100644 index 0000000..8c7f463 --- /dev/null +++ b/src/libnpsharp/RPC/Packets/StorageWriteUserFileResultMessage.cs @@ -0,0 +1,18 @@ +using ProtoBuf; + +namespace NPSharp.RPC.Packets +{ + [Packet(1113)] + [ProtoContract] + class StorageWriteUserFileResultMessage : RPCServerMessage + { + [ProtoMember(1)] + public int Result { get; set; } + + [ProtoMember(2)] + public string FileName { get; set; } + + [ProtoMember(3)] + public ulong NPID { get; set; } + } +} \ No newline at end of file diff --git a/src/libnpsharp/RPC/RPCClientStream.cs b/src/libnpsharp/RPC/RPCClientStream.cs new file mode 100644 index 0000000..c09522c --- /dev/null +++ b/src/libnpsharp/RPC/RPCClientStream.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using NPSharp.RPC.Packets; + +namespace NPSharp.RPC +{ + /// + /// Represents a low-level client stream which can communicate with an NP server using RPC packets. + /// + public class RPCClientStream + { + private NetworkStream _ns; + private uint _id; + + private readonly string _host; + private readonly ushort _port; + + private readonly Dictionary> _callbacks = new Dictionary>(); + + /// + /// Initializes an RPC connection stream with a specified host and port. + /// + /// The host to connect to. + /// The port to use. Default: 3025. + public RPCClientStream(string host, ushort port = 3025) + { + _host = host; + _port = port; + } + + /// + /// Opens the RPC stream to the NP server. + /// + /// True if the connection succeeded, otherwise false. + public bool Open() + { + // Connection already established? + if (_ns != null) + throw new InvalidOperationException("Connection already opened"); + + var tcp = new TcpClient(); + try + { + tcp.Connect(_host, _port); + } + catch + { + return false; + } + _ns = tcp.GetStream(); + return true; + } + + /// + /// Closes the connection with the NP server. + /// + /// + public void Close(int timeout = 2000) + { + // Connection already closed? + if (_ns == null) + throw new InvalidOperationException("Connection already closed"); + + try + { + _ns.Close(timeout); + _ns.Dispose(); + } + finally + { + _ns = null; + } + } + + /// + /// Attaches a callback to the next message being sent out. This allows handling response packets. + /// + /// The method to call when we receive a response to the next message + public void AttachCallback(Action callback) + { + if (_callbacks.ContainsKey(_id)) + throw new Exception("There is already a callback for the current message. You can only add max. one callback."); + _callbacks.Add(_id, callback); + } + + // TODO: Exposure of message ID needed or no? + /// + /// Sends out an RPC message. + /// + /// The RPC message to send out. + /// The new ID of the message. + public uint Send(RPCClientMessage message) + { + if (_ns == null) + throw new InvalidOperationException("You need to open the stream first."); + + var buffer = message.Serialize(_id); + + _ns.Write(buffer, 0, buffer.Length); + + return _id++; + } + + /// + /// Waits for the next RPC message from the server and reads it. + /// + /// The received server message. + public RPCServerMessage Read() + { + if (_ns == null) + throw new InvalidOperationException("You need to open the stream first."); + + var message = RPCServerMessage.Deserialize(_ns); + + if (message == null) + return null; + + if (!_callbacks.ContainsKey(message.MessageId)) + return message; + + _callbacks[message.MessageId].Invoke(message); + _callbacks.Remove(message.MessageId); + + return message; + } + } +}