From dd0774d2014110e542edb23573636c9baba3bfd3 Mon Sep 17 00:00:00 2001 From: Rodrigo Appelt Date: Wed, 27 Dec 2023 02:55:29 -0300 Subject: [PATCH] packets and builders --- Unichain.P2P/Packets/Content.cs | 68 +++++++++++++ Unichain.P2P/Packets/ContentBuilder.cs | 46 +++++++++ Unichain.P2P/Packets/Packets.md | 98 ++++++++++++++++++ Unichain.P2P/Packets/Request.cs | 42 ++++++++ Unichain.P2P/Packets/RequestBuilder.cs | 126 ++++++++++++++++++++++++ Unichain.P2P/Packets/RequestMethod.cs | 7 ++ Unichain.P2P/Packets/Response.cs | 65 ++++++++++++ Unichain.P2P/Packets/ResponseBuilder.cs | 50 ++++++++++ Unichain.P2P/Packets/StatusCode.cs | 9 ++ Unichain.P2P/Request.cs | 57 ----------- Unichain.P2P/RequestMethod.cs | 13 --- Unichain.P2P/Response.cs | 28 ------ Unichain.P2P/StatusCode.cs | 15 --- Unichain.P2P/TcpNode.cs | 5 +- Unichain.P2P/UnichainNode.cs | 15 ++- 15 files changed, 528 insertions(+), 116 deletions(-) create mode 100644 Unichain.P2P/Packets/Content.cs create mode 100644 Unichain.P2P/Packets/ContentBuilder.cs create mode 100644 Unichain.P2P/Packets/Packets.md create mode 100644 Unichain.P2P/Packets/Request.cs create mode 100644 Unichain.P2P/Packets/RequestBuilder.cs create mode 100644 Unichain.P2P/Packets/RequestMethod.cs create mode 100644 Unichain.P2P/Packets/Response.cs create mode 100644 Unichain.P2P/Packets/ResponseBuilder.cs create mode 100644 Unichain.P2P/Packets/StatusCode.cs delete mode 100644 Unichain.P2P/Request.cs delete mode 100644 Unichain.P2P/RequestMethod.cs delete mode 100644 Unichain.P2P/Response.cs delete mode 100644 Unichain.P2P/StatusCode.cs diff --git a/Unichain.P2P/Packets/Content.cs b/Unichain.P2P/Packets/Content.cs new file mode 100644 index 0000000..ad8b21e --- /dev/null +++ b/Unichain.P2P/Packets/Content.cs @@ -0,0 +1,68 @@ +using System.Text; + +namespace Unichain.P2P.Packets; + +/// +/// Represents a collection of headers and a payload +/// +public struct Content { + + /// + /// The headers of this content. + /// + public Dictionary Headers { get; set; } + + /// + /// The binary payload of this content. + /// + public byte[] Payload { get; set; } + + /// + /// Writes the current content to the stream + /// + /// The stream to be written onto + /// If the stream is non writable + internal readonly void Write(Stream s) { + if (!s.CanWrite) { + throw new NotSupportedException("Cannot write to stream"); + } + + using BinaryWriter bw = new(s, Encoding.UTF8, true); + + bw.Write(Headers.Count); + foreach (var header in Headers) { + bw.Write(header.Key); + bw.Write(header.Value); + } + bw.Write((uint)Payload.Length); + bw.Write(Payload); + } + + /// + /// Reads and creates a new content from the stream + /// + /// The stream that has the data + /// The newly created content + /// If the stream is non readable + internal static Content Read(Stream s) { + if (!s.CanRead) { + throw new NotSupportedException("Cannot read from stream"); + } + + using BinaryReader br = new(s, Encoding.UTF8, true); + + var headers = new Dictionary(); + int headerCount = br.ReadInt32(); + for (int i = 0; i < headerCount; i++) { + string key = br.ReadString(); + string value = br.ReadString(); + headers.Add(key, value); + } + uint payloadSize = br.ReadUInt32(); + byte[] payload = br.ReadBytes((int)payloadSize); + return new Content { + Headers = headers, + Payload = payload + }; + } +} diff --git a/Unichain.P2P/Packets/ContentBuilder.cs b/Unichain.P2P/Packets/ContentBuilder.cs new file mode 100644 index 0000000..abd14e2 --- /dev/null +++ b/Unichain.P2P/Packets/ContentBuilder.cs @@ -0,0 +1,46 @@ +namespace Unichain.P2P.Packets; + +/// +/// A class to create new contents for requests and responses. +/// +public class ContentBuilder { + private readonly Dictionary headers; + private byte[] payload; + + /// + /// Instantiates a new builder for objects with default information. + /// + public ContentBuilder() { + headers = new(); + payload = Array.Empty(); + } + + /// + /// Adds a new header to the content. + /// + /// The key of the header + /// The value of the header + public ContentBuilder WithHeader(string key, string value) { + headers.Add(key, value); + return this; + } + + /// + /// Defines what the payload will be. + /// + /// The payload + public ContentBuilder WithPayload(byte[] payload) { + this.payload = payload; + return this; + } + + /// + /// Builds the final object + /// + public Content Build() { + return new() { + Headers = headers, + Payload = payload + }; + } +} diff --git a/Unichain.P2P/Packets/Packets.md b/Unichain.P2P/Packets/Packets.md new file mode 100644 index 0000000..e297e8e --- /dev/null +++ b/Unichain.P2P/Packets/Packets.md @@ -0,0 +1,98 @@ +# Unichain.P2P + +## Request + +A request packet is used to ask for information from +another node or send important data. It also can be a broadcast, +where the sender should not expect a response and must send +the same request for all its known peers. + +A request is made of primarily of many parts: + +* The current protocol version that the sender is using. +* A [RequestMethod](./RequestMethod.cs). Similar to HTTP's GET, POST, etc. +* A Route. It is a path that identifies what endpoint in the node this +request should be sent to. +* Information about the sender. For maximum compatibility, it should include +private and public IP, port and a unique node indentifier. This information +is used to enable nodes in the same computer or same networks to efficiently +communicate with each other. +* Whether this request is originating from a broadcast request or not. +* A collection of contents. Each content has a set of headers and a payload. + +### Structure + +The sequence structure of a request is as follows: + +> **Note:** All strings in the payload have a size prefix, so you +shouldn't read string until a null terminator(\0) is found. + +| Order | Description | Type | Size | +| ----- | ----------- | ---- | ---- | +| 1 | Protocol Version | int | 4 bytes | +| 2 | Request Method | int | 4 bytes | +| 3 | Route | string | Variable | +| 4 | Ip Version(IPV4/IPV6) | bool | 1 byte | +| 5 | Public Ip | string | Variable | +| 6 | Private Ip(empty if IPV6) | string | Variable | +| 7 | Is Broadcast | bool | 1 byte | +| 8 | Contents Count | int | 4 bytes | +| 9 | Contents | Content[] | Variable | + +## Content + +A packet content is merely a collection of headers and a payload. +An entire content can be read/written with funcions from the +struct [Content](./Content.cs) itself. + +### Structure + +| Order | Description | Type | Size | +| ----- | ----------- | ---- | ---- | +| 1 | Headers Count | int | 4 bytes | +| 2 | Headers | Header[] | Variable | +| 3 | Payload Length | int | 4 bytes | +| 4 | Payload | byte[] | Variable | + +## Content Headers + +The headers are included in each content to identify the content's type +and any other metadata that is needed to process the content. It can be +the file name, in case it is an attachment, the priority of a new +transaction, compression algorithm of the payload, encryption, etc. + +A header is constituted of a key and a value. Both are strings. Beware +that can be a limit of key/value sizes, as it is prudent to keep them +at a minimum size to ensure fast communication. + +### Structure + +The strings here could omit the size prefix, as they are already +included in the content structure, but are included for simplicity. + +| Order | Description | Type | Size | +| ----- | ----------- | ---- | ---- | +| 1 | Key | string | Variable | +| 2 | Value | string | Variable | + + +## Content Payload + +The content payload is basically the data being sent. It is an unsigned +integer indicating the size of the payload, followed by the payload itself. +Note that the payload may be compressed and/or encrypted, depending on the +headers information. + +## Response + +A response is simpler than a request as it has a limitation of only one +content. It also has a status code, indicating whether the request was +successful or an error occurred. + +### Structure + +| Order | Description | Type | Size | +| ----- | ----------- | ---- | ---- | +| 1 | Protocol Version | int | 4 bytes | +| 2 | Status Code | int | 4 bytes | +| 3 | Content | Content | Variable | diff --git a/Unichain.P2P/Packets/Request.cs b/Unichain.P2P/Packets/Request.cs new file mode 100644 index 0000000..e5425a9 --- /dev/null +++ b/Unichain.P2P/Packets/Request.cs @@ -0,0 +1,42 @@ +namespace Unichain.P2P.Packets; + +/// +/// Represents a request method from a node +/// +public struct Request +{ + /// + /// The protocol version that the sender is using. + /// + public int ProtocolVersion { get; set; } + + /// + /// The method of the request + /// + public RequestMethod Method { get; set; } + + /// + /// The URI of the request + /// + public Route Route { get; set; } + + /// + /// Identification information of the sender + /// + public Address Sender { get; set; } + + /// + /// Defines if the current request is a broadcast request. If true, + /// the node should not send a response and should spread it across + /// all its known peers. + /// + public bool IsBroadcast { get; set; } + + /// + /// A list with all the present in this request. + /// + public List Contents { get; set; } + + // TODO: implement read/write in request + // must implement Address read/write first and public/private IP stuff +} diff --git a/Unichain.P2P/Packets/RequestBuilder.cs b/Unichain.P2P/Packets/RequestBuilder.cs new file mode 100644 index 0000000..ba3cb8a --- /dev/null +++ b/Unichain.P2P/Packets/RequestBuilder.cs @@ -0,0 +1,126 @@ +namespace Unichain.P2P.Packets; + +/// +/// A class to craft new requests. +/// +public class RequestBuilder { + private int protocolVersion; + private RequestMethod method; + private Route? route; + private Address? sender; + private bool isBroadcast; + private List contents; + + /// + /// Creates a new builder for objects with default information. + /// + public RequestBuilder() + { + contents = new(); + } + + /// + /// Defines the protocol version that the request will use + /// + /// The protocol version + public RequestBuilder WithProtocolVersion(int protocolVersion) { + this.protocolVersion = protocolVersion; + return this; + } + + /// + /// Defines the method that the request will use + /// + /// The method + public RequestBuilder WithMethod(RequestMethod method) { + this.method = method; + return this; + } + + /// + /// Defines the route of the request + /// + /// The route that this request will be forwarded + public RequestBuilder WithRoute(Route route) { + this.route = route; + return this; + } + + /// + /// Defines the sender of the request + /// + /// The sender + public RequestBuilder WithSender(Address sender) { + this.sender = sender; + return this; + } + + /// + /// Defines a request to be broadcasted across the entire network + /// + public RequestBuilder WithBroadcast() { + this.isBroadcast = true; + return this; + } + + /// + /// Sets a specific state for the to be broadcasted or not + /// + /// If the request should be a broadcast or not + public RequestBuilder WithBroadcast(bool isBroadcast) { + this.isBroadcast = isBroadcast; + return this; + } + + /// + /// Adds many to the request at once. + /// This method will override any previous contents added to the request. + /// + /// The contents to be added + public RequestBuilder WithContents(List contents) { + this.contents = contents; + return this; + } + + /// + /// Adds many to the request at once + /// using an , preferably a lambda expression + /// + /// The action that adds all contents + public RequestBuilder WithContents(Action> contents) { + contents(this.contents); + return this; + } + + /// + /// Adds a to the request + /// + /// The content to be added + public RequestBuilder WithContent(Content content) { + this.contents.Add(content); + return this; + } + + /// + /// Creates a object with the current builder configuration + /// + /// If the route or sender is not defined + public Request Build() { + if(route is null) { + throw new InvalidOperationException("The route must be defined"); + } + + if(sender is null) { + throw new InvalidOperationException("The sender must be defined"); + } + + return new Request { + ProtocolVersion = protocolVersion, + Method = method, + Route = route, + Sender = sender, + IsBroadcast = isBroadcast, + Contents = contents + }; + } +} diff --git a/Unichain.P2P/Packets/RequestMethod.cs b/Unichain.P2P/Packets/RequestMethod.cs new file mode 100644 index 0000000..a28f043 --- /dev/null +++ b/Unichain.P2P/Packets/RequestMethod.cs @@ -0,0 +1,7 @@ +namespace Unichain.P2P; + +public enum RequestMethod { + INVALID, + GET, + POST +} diff --git a/Unichain.P2P/Packets/Response.cs b/Unichain.P2P/Packets/Response.cs new file mode 100644 index 0000000..b261c51 --- /dev/null +++ b/Unichain.P2P/Packets/Response.cs @@ -0,0 +1,65 @@ +using System.Text; + +namespace Unichain.P2P.Packets; + +/// +/// Represents a response for a request +/// +public struct Response { + + /// + /// The protocol version that this node is using. Used to the sender + /// troubleshoot issues if he has an different version of the protocol. + /// + public int ProtocolVersion { get; set; } + + /// + /// The status code of the response + /// + public StatusCode StatusCode { get; set; } + + /// + /// The Base64 encoded payload of the response + /// + public Content Content { get; set; } + + /// + /// Writes the response to a stream. + /// + /// The destination stream + /// If the is non writable + internal readonly void Write(Stream s) { + if (!s.CanWrite) { + throw new NotSupportedException("Cannot write to stream"); + } + + using BinaryWriter bw = new(s, Encoding.UTF8, true); + + bw.Write(ProtocolVersion); + bw.Write((int)StatusCode); + Content.Write(s); + } + + /// + /// Reads and creates a Response object from a stream + /// + /// The stream that has the data + /// The object created + /// If the is non readable + internal static Response Read(Stream s) { + if (!s.CanRead) { + throw new NotSupportedException("Cannot read from stream"); + } + + using BinaryReader br = new(s, Encoding.UTF8, true); + + int protocolVersion = br.ReadInt32(); + StatusCode statusCode = (StatusCode)br.ReadInt32(); + Content content = Content.Read(s); + return new Response { + ProtocolVersion = protocolVersion, + StatusCode = statusCode, + Content = content + }; + } +} diff --git a/Unichain.P2P/Packets/ResponseBuilder.cs b/Unichain.P2P/Packets/ResponseBuilder.cs new file mode 100644 index 0000000..e00c8fe --- /dev/null +++ b/Unichain.P2P/Packets/ResponseBuilder.cs @@ -0,0 +1,50 @@ +namespace Unichain.P2P.Packets; + +/// +/// A class to craft new responses. +/// +public class ResponseBuilder { + private int protocolVersion; + private StatusCode statusCode; + private Content content; + + /// + /// Defines the protocol version that the response will use + /// + /// The protocol version used + public ResponseBuilder WithProtocolVersion(int protocolVersion) { + this.protocolVersion = protocolVersion; + return this; + } + + /// + /// Sets the status code of the response + /// + /// The status code + public ResponseBuilder WithStatusCode(StatusCode statusCode) { + this.statusCode = statusCode; + return this; + } + + /// + /// Defines the content of the response + /// + /// The content of the response + public ResponseBuilder WithContent(Content content) { + this.content = content; + return this; + } + + /// + /// Creates a new object with the configurations + /// provided + /// + /// The new response object + public Response Build() { + return new Response { + ProtocolVersion = protocolVersion, + StatusCode = statusCode, + Content = content + }; + } +} diff --git a/Unichain.P2P/Packets/StatusCode.cs b/Unichain.P2P/Packets/StatusCode.cs new file mode 100644 index 0000000..02a7c02 --- /dev/null +++ b/Unichain.P2P/Packets/StatusCode.cs @@ -0,0 +1,9 @@ +namespace Unichain.P2P; + +public enum StatusCode { + Invalid = 0, + OK = 200, + BadRequest = 400, + NotFound = 404, + InternalServerError = 500 +} diff --git a/Unichain.P2P/Request.cs b/Unichain.P2P/Request.cs deleted file mode 100644 index 6c6a4e9..0000000 --- a/Unichain.P2P/Request.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; - -namespace Unichain.P2P; - -/// -/// Represents a request method from a node -/// -public class Request { - - /// - /// The method of the request - /// - public required RequestMethod Method { get; set; } - - /// - /// The address of the sender - /// - public Address Sender { get; set; } - - /// - /// Defines if this request is a broadcast. It should be propagated across the network and - /// a response should not matter. - /// - public bool IsBroadcast { get; set; } - - /// - /// The URI of the request - /// - public required Route Route { get; set; } - - /// - /// The Base64 encoded payload of the request - /// - public string Payload { get; set; } = ""; - - /// - /// Shortcut to get the payload data if it is a text - /// - public string TextPayload => Encoding.UTF8.GetString(Convert.FromBase64String(Payload)); - - [Obsolete("Use the parameterless constructor.")] - public Request(RequestMethod method, Route route, string payload, IPEndPoint sender) { - Method = method; - Route = route; - Payload = payload; - Sender = new Address(sender.Address.ToString(), sender.Port); - } - - public Request() { - - } -} diff --git a/Unichain.P2P/RequestMethod.cs b/Unichain.P2P/RequestMethod.cs deleted file mode 100644 index 58a7cb4..0000000 --- a/Unichain.P2P/RequestMethod.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Unichain.P2P { - public enum RequestMethod { - INVALID, - GET, - POST - } -} diff --git a/Unichain.P2P/Response.cs b/Unichain.P2P/Response.cs deleted file mode 100644 index 094eaec..0000000 --- a/Unichain.P2P/Response.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Unichain.P2P; - -/// -/// Represents a response for a request -/// -public class Response { - - /// - /// The status code of the response - /// - public StatusCode StatusCode { get; set; } - - /// - /// The Base64 encoded payload of the response - /// - public string Payload { get; set; } - - public Response(StatusCode statusCode, string payload) { - StatusCode = statusCode; - Payload = payload; - } -} diff --git a/Unichain.P2P/StatusCode.cs b/Unichain.P2P/StatusCode.cs deleted file mode 100644 index a893efd..0000000 --- a/Unichain.P2P/StatusCode.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Unichain.P2P { - public enum StatusCode { - Invalid = 0, - OK = 200, - BadRequest = 400, - NotFound = 404, - InternalServerError = 500 - } -} diff --git a/Unichain.P2P/TcpNode.cs b/Unichain.P2P/TcpNode.cs index eb0e8f2..cc93819 100644 --- a/Unichain.P2P/TcpNode.cs +++ b/Unichain.P2P/TcpNode.cs @@ -11,8 +11,9 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Unichain.P2P.Packets; -namespace Unichain.P2P; +namespace Unichain.P2P; public abstract class TcpNode { #region Variables @@ -117,6 +118,8 @@ public async Task Stop() { /// The client that sent the request /// The request object protected Request ReadRequest(TcpClient client) { + // TODO: extract stream only and forward to local function on struct + NetworkStream inStream = client.GetStream(); using BinaryReader reader = new(inStream, Encoding.UTF8, true); diff --git a/Unichain.P2P/UnichainNode.cs b/Unichain.P2P/UnichainNode.cs index 2d9e2a2..fe3c01d 100644 --- a/Unichain.P2P/UnichainNode.cs +++ b/Unichain.P2P/UnichainNode.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Unichain.P2P.Packets; namespace Unichain.P2P; @@ -42,7 +43,9 @@ protected override Response Process(Request request) { } else if (path == Route.Peers_Join && method == RequestMethod.POST) { response = RegisterNewPeer(request); } else { - response = new Response(StatusCode.NotFound, ""); + response = new ResponseBuilder() + .WithStatusCode(StatusCode.NotFound) + .Build(); } return response; } @@ -57,7 +60,15 @@ private Response GetPeers() { peersSent.Add(new Address("localhost", port)); var json = JsonSerializer.Serialize(peersSent); var bytes = Encoding.UTF8.GetBytes(json); - return new Response(StatusCode.OK, Convert.ToBase64String(bytes)); + ResponseBuilder builder = new(); + Response response = builder + .WithStatusCode(StatusCode.OK) + .WithContent(new ContentBuilder() + .WithHeader("contentType","json") + .WithPayload(bytes) + .Build()) + .Build(); + return response; } private Response RegisterNewPeer(Request request) {