Skip to content

Commit

Permalink
packets and builders
Browse files Browse the repository at this point in the history
  • Loading branch information
Agentew04 committed Dec 27, 2023
1 parent ef2dc72 commit dd0774d
Show file tree
Hide file tree
Showing 15 changed files with 528 additions and 116 deletions.
68 changes: 68 additions & 0 deletions Unichain.P2P/Packets/Content.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Text;

namespace Unichain.P2P.Packets;

/// <summary>
/// Represents a collection of headers and a payload
/// </summary>
public struct Content {

/// <summary>
/// The headers of this content.
/// </summary>
public Dictionary<string,string> Headers { get; set; }

/// <summary>
/// The binary payload of this content.
/// </summary>
public byte[] Payload { get; set; }

/// <summary>
/// Writes the current content to the stream
/// </summary>
/// <param name="s">The stream to be written onto</param>
/// <exception cref="NotSupportedException">If the stream is non writable</exception>
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);
}

/// <summary>
/// Reads and creates a new content from the stream
/// </summary>
/// <param name="s">The stream that has the data</param>
/// <returns>The newly created content</returns>
/// <exception cref="NotSupportedException">If the stream is non readable</exception>
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<string, string>();
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
};
}
}
46 changes: 46 additions & 0 deletions Unichain.P2P/Packets/ContentBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace Unichain.P2P.Packets;

/// <summary>
/// A class to create new contents for requests and responses.
/// </summary>
public class ContentBuilder {
private readonly Dictionary<string, string> headers;
private byte[] payload;

/// <summary>
/// Instantiates a new builder for <see cref="Content"/> objects with default information.
/// </summary>
public ContentBuilder() {
headers = new();
payload = Array.Empty<byte>();
}

/// <summary>
/// Adds a new header to the content.
/// </summary>
/// <param name="key">The key of the header</param>
/// <param name="value">The value of the header</param>
public ContentBuilder WithHeader(string key, string value) {
headers.Add(key, value);
return this;
}

/// <summary>
/// Defines what the payload will be.
/// </summary>
/// <param name="payload">The payload</param>
public ContentBuilder WithPayload(byte[] payload) {
this.payload = payload;
return this;
}

/// <summary>
/// Builds the final <see cref="Content"/> object
/// </summary>
public Content Build() {
return new() {
Headers = headers,
Payload = payload
};
}
}
98 changes: 98 additions & 0 deletions Unichain.P2P/Packets/Packets.md
Original file line number Diff line number Diff line change
@@ -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 |
42 changes: 42 additions & 0 deletions Unichain.P2P/Packets/Request.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Unichain.P2P.Packets;

/// <summary>
/// Represents a request method from a node
/// </summary>
public struct Request
{
/// <summary>
/// The protocol version that the sender is using.
/// </summary>
public int ProtocolVersion { get; set; }

/// <summary>
/// The method of the request
/// </summary>
public RequestMethod Method { get; set; }

/// <summary>
/// The URI of the request
/// </summary>
public Route Route { get; set; }

/// <summary>
/// Identification information of the sender
/// </summary>
public Address Sender { get; set; }

/// <summary>
/// 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.
/// </summary>
public bool IsBroadcast { get; set; }

/// <summary>
/// A list with all the <see cref="Content"/> present in this request.
/// </summary>
public List<Content> Contents { get; set; }

// TODO: implement read/write in request
// must implement Address read/write first and public/private IP stuff
}
126 changes: 126 additions & 0 deletions Unichain.P2P/Packets/RequestBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
namespace Unichain.P2P.Packets;

/// <summary>
/// A class to craft new requests.
/// </summary>
public class RequestBuilder {
private int protocolVersion;
private RequestMethod method;
private Route? route;
private Address? sender;
private bool isBroadcast;
private List<Content> contents;

/// <summary>
/// Creates a new builder for <see cref="Request"/> objects with default information.
/// </summary>
public RequestBuilder()
{
contents = new();
}

/// <summary>
/// Defines the protocol version that the request will use
/// </summary>
/// <param name="protocolVersion">The protocol version</param>
public RequestBuilder WithProtocolVersion(int protocolVersion) {
this.protocolVersion = protocolVersion;
return this;
}

/// <summary>
/// Defines the method that the request will use
/// </summary>
/// <param name="method">The method</param>
public RequestBuilder WithMethod(RequestMethod method) {
this.method = method;
return this;
}

/// <summary>
/// Defines the route of the request
/// </summary>
/// <param name="route">The route that this request will be forwarded</param>
public RequestBuilder WithRoute(Route route) {
this.route = route;
return this;
}

/// <summary>
/// Defines the sender of the request
/// </summary>
/// <param name="sender">The sender</param>
public RequestBuilder WithSender(Address sender) {
this.sender = sender;
return this;
}

/// <summary>
/// Defines a request to be broadcasted across the entire network
/// </summary>
public RequestBuilder WithBroadcast() {
this.isBroadcast = true;
return this;
}

/// <summary>
/// Sets a specific state for the <see cref="Request"/> to be broadcasted or not
/// </summary>
/// <param name="isBroadcast">If the request should be a broadcast or not</param>
public RequestBuilder WithBroadcast(bool isBroadcast) {
this.isBroadcast = isBroadcast;
return this;
}

/// <summary>
/// Adds many <see cref="Content"/> to the request at once.
/// <b>This method will override any previous contents added to the request.</b>
/// </summary>
/// <param name="contents">The contents to be added</param>
public RequestBuilder WithContents(List<Content> contents) {
this.contents = contents;
return this;
}

/// <summary>
/// Adds many <see cref="Content"/> to the request at once
/// using an <see cref="Action"/>, preferably a lambda expression
/// </summary>
/// <param name="contents">The action that adds all contents</param>
public RequestBuilder WithContents(Action<List<Content>> contents) {
contents(this.contents);
return this;
}

/// <summary>
/// Adds a <see cref="Content"/> to the request
/// </summary>
/// <param name="content">The content to be added</param>
public RequestBuilder WithContent(Content content) {
this.contents.Add(content);
return this;
}

/// <summary>
/// Creates a <see cref="Request"/> object with the current builder configuration
/// </summary>
/// <exception cref="InvalidOperationException">If the route or sender is not defined</exception>
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
};
}
}
7 changes: 7 additions & 0 deletions Unichain.P2P/Packets/RequestMethod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Unichain.P2P;

public enum RequestMethod {
INVALID,
GET,
POST
}
Loading

0 comments on commit dd0774d

Please sign in to comment.