Skip to content

Commit

Permalink
Added SSL example, bug fixes and upgrade refactor
Browse files Browse the repository at this point in the history
- Fixed a bug where the PeerConfiguration constructor ignored the protocolMode argument and always set the mode to active
- Changed the way UpgradeAsync works, it now requires that the peer be in Passive mode, simplifying the logic but potentially a breaking change
- Added SslUpgrader.CheckCertificateRevocation
- Added SslUpgrader.RemoteValidationCallback
- Added SslUpgrader.LocalSelectionCallback
- Added an SSL text-protocol example
- Moved to 0.6.0
  • Loading branch information
alandoherty committed Sep 6, 2019
1 parent 39667ca commit 2fd95d1
Show file tree
Hide file tree
Showing 14 changed files with 388 additions and 42 deletions.
9 changes: 8 additions & 1 deletion ProtoSocket.sln
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.Chat", "samples\Exa
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.Minecraft", "samples\Example.Minecraft\Example.Minecraft.csproj", "{D9E913D0-B075-489A-96AE-7389C86C790E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.Line", "samples\Example.Line\Example.Line.csproj", "{5C1CEDDA-43A7-4501-BF28-E4A8BC142574}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.Line", "samples\Example.Line\Example.Line.csproj", "{5C1CEDDA-43A7-4501-BF28-E4A8BC142574}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.Ssl", "samples\Example.Ssl\Example.Ssl.csproj", "{8387CAE0-ADDC-4B06-BB97-17190D788EFE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -40,6 +42,10 @@ Global
{5C1CEDDA-43A7-4501-BF28-E4A8BC142574}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C1CEDDA-43A7-4501-BF28-E4A8BC142574}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C1CEDDA-43A7-4501-BF28-E4A8BC142574}.Release|Any CPU.Build.0 = Release|Any CPU
{8387CAE0-ADDC-4B06-BB97-17190D788EFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8387CAE0-ADDC-4B06-BB97-17190D788EFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8387CAE0-ADDC-4B06-BB97-17190D788EFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8387CAE0-ADDC-4B06-BB97-17190D788EFE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -48,6 +54,7 @@ Global
{3244FC3C-5703-4B56-A754-CC6D7DB87039} = {232DEEE3-098B-46F0-803A-591781CEB430}
{D9E913D0-B075-489A-96AE-7389C86C790E} = {232DEEE3-098B-46F0-803A-591781CEB430}
{5C1CEDDA-43A7-4501-BF28-E4A8BC142574} = {232DEEE3-098B-46F0-803A-591781CEB430}
{8387CAE0-ADDC-4B06-BB97-17190D788EFE} = {232DEEE3-098B-46F0-803A-591781CEB430}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {056A9443-6614-4870-A4BA-EE86AF7BF8C8}
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ await client.UpgradeAsync(upgrader);
client.Mode = ProtocolMode.Active;
```

You can find an example of using SSL [here](samples/Example.Ssl).

### Statistics

In the newer versions of ProtoSocket you can request network statistics from the peer without dynamic allocation.
Expand Down
Binary file added docs/img/example_ssl.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions samples/Example.Ssl/Example.Ssl.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ProtoSocket\ProtoSocket.csproj" />
</ItemGroup>

</Project>
74 changes: 74 additions & 0 deletions samples/Example.Ssl/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

namespace Example.Ssl
{
class Program
{
static Task Main(string[] args)
{
Server();
return ClientAsync();
}

static void Server()
{
// configure the server
SslServer server = new SslServer(new X509Certificate2("ssl.p12"));
server.Configure("tcp://127.0.0.1:3001");

server.Connected += (o, e) => {
Console.WriteLine($"srv:{e.Peer.RemoteEndPoint}: connected");
};

server.Disconnected += (o, e) => {
Console.WriteLine($"srv:{e.Peer.RemoteEndPoint}: disconnected");
};

// start the server
server.Start();
}

static async Task ClientAsync()
{
// try and connect three times, on the third time we will show an error
SslClient client = null;

for (int i = 0; i < 3; i++) {
client = new SslClient();

try {
await client.ConnectAsync(new Uri("tcp://127.0.0.1:3001"))
.ConfigureAwait(false);
break;
} catch(Exception ex) {
if (i == 2) {
Console.Error.WriteLine($"client:{ex.ToString()}");
return;
} else {
await Task.Delay(1000)
.ConfigureAwait(false);
}
}
}

// show a basic read line prompt, sending every frame to the server once enter is pressed
string line = null;

do {
// read a line of data
Console.Write("> ");
line = (await Console.In.ReadLineAsync().ConfigureAwait(false)).Trim();

// send
await client.SendAsync(line)
.ConfigureAwait(false);

// wait for reply
await client.ReceiveAsync()
.ConfigureAwait(false);
} while (!line.Equals("exit", StringComparison.OrdinalIgnoreCase));
}
}
}
26 changes: 26 additions & 0 deletions samples/Example.Ssl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SSL Example

This example shows how to use SSL to secure your network traffic, this example does not verify that the certificate is signed by a CA but this is trivial to add.

The example also provides a good `IProtocolCoder` implementation which can be extended to support any length-prefixed frame based protocol.

Note that this example is not production ready, it does not perform any clientside validation that the certificate is trusted. Infact is specifically ignores any validation and will accept any certificate, some familiarity with `SslStream` will help translate to using `SslUpgrader` effectively.

### Generating a certificate

The example expects an X509 container named `ssl.p12` in the working directory when running, you can generate a (unsecured) p12 container using the following commands. This container must hold the private key as well as the public key, secured with no password.

```
openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out ssl.pem
openssl pkcs12 -inkey key.pem -in ssl.pem -export -out ssl.p12
```

#### Notes

In order to upgrade your `ProtocolPeer`, you will need to explicitly configure the peer to use `ProtocolMode.Passive`. Otherwise the peer will start reading SSL layer frames as soon as the connection has been made, this is intended behaviour when `ProtocolMode.Active` is set but causes issues when you need to handoff negociation to an external library.

## Usage

Once a valid certificate has been generated you can type chat messages to the server, which will print them out.

![Example Ssl](../../docs/img/example_ssl.png)
38 changes: 38 additions & 0 deletions samples/Example.Ssl/SslClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using ProtoSocket;
using ProtoSocket.Upgraders;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace Example.Ssl
{
public class SslClient : ProtocolClient<string>
{
public override async Task ConnectAsync(Uri uri)
{
// connect to the server
await base.ConnectAsync(uri).ConfigureAwait(false);

// upgrade, this isn't setup to verify trust correctly and will blindly accept any certificate
// DO NOT USE IN PRODUCTION
try {
SslUpgrader upgrader = new SslUpgrader(uri.Host);
upgrader.RemoteValidationCallback = (o, crt, cert, sse) => {
return true;
};

await UpgradeAsync(upgrader).ConfigureAwait(false);
} catch(Exception) {
Dispose();
throw;
}

// enable active mode so frames start being read by ProtoSocket
Mode = ProtocolMode.Active;
}

public SslClient() : base(new SslCoder(), new PeerConfiguration(ProtocolMode.Passive)) {
}
}
}
130 changes: 130 additions & 0 deletions samples/Example.Ssl/SslCoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using ProtoSocket;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Text;

namespace Example.Ssl
{
public class SslCoder : IProtocolCoder<string>
{
private byte[] _bytes;
private int _bytesLength;
private int _bytesOffset;
private State _state;

public bool Read(PipeReader reader, CoderContext<string> ctx, out string frame)
{
if (reader.TryRead(out ReadResult result) && !result.IsCompleted) {
// get the sequence buffer
ReadOnlySequence<byte> buffer = result.Buffer;

try {
while (buffer.Length > 0) {
if (_state == State.Size) {
if (buffer.Length >= 2) {
// copy length from buffer
Span<byte> lengthBytes = stackalloc byte[2];

buffer.Slice(0, 2)
.CopyTo(lengthBytes);

int length = BitConverter.ToUInt16(lengthBytes);

if (length > 32768)
throw new ProtocolCoderException("The client sent an invalid frame length");

// increment the amount we were able to copy in
buffer = buffer.Slice(2);

// move state to content if we have a message with data
if (length > 0) {
_bytes = ArrayPool<byte>.Shared.Rent(length);
_bytesLength = length;
_bytesOffset = 0;
_state = State.Content;
} else {
frame = string.Empty;
return true;
}
} else {
break;
}
} else if (_state == State.Content) {
if (buffer.Length >= 1) {
// figure out how much data we can read, and how much we actually have to read
int remainingBytes = _bytesLength - _bytesOffset;
int maxBytes = Math.Min((int)buffer.Length, remainingBytes);

// copy into buffer
buffer.Slice(0, maxBytes)
.CopyTo(_bytes.AsSpan(_bytesOffset, maxBytes));

// increment offset by amount we copied
_bytesOffset += maxBytes;
buffer = buffer.Slice(maxBytes);

// if we have filled the content array we can now produce a frame
if (_bytesOffset == _bytesLength) {
try {
frame = Encoding.UTF8.GetString(_bytes);
_state = State.Size;
return true;
} finally {
ArrayPool<byte>.Shared.Return(_bytes);
_bytes = null;
}
}
}
}
}
} finally {
reader.AdvanceTo(buffer.GetPosition(0), buffer.End);
}
}

// we didn't find a frame
frame = default;
return false;
}

public void Reset()
{
throw new NotSupportedException();
}

public void Write(Stream stream, string frame, CoderContext<string> ctx)
{
// encode the frame into a UTF8 byte array
byte[] frameBytes = Encoding.UTF8.GetBytes(frame);

if (frameBytes.Length > 32768)
throw new ProtocolCoderException("The frame is too large to write");

// encode the length
byte[] lengthBytes = BitConverter.GetBytes((ushort)frame.Length);

// write to stream
stream.Write(lengthBytes);
stream.Write(frameBytes);
}

/// <summary>
/// Defines the states for this coder.
/// </summary>
enum State
{
Size,
Content
}

~SslCoder()
{
// return the array back to the pool if we deconstruct before finishing the entire frame
if (_bytes != null)
ArrayPool<byte>.Shared.Return(_bytes);
}
}
}
47 changes: 47 additions & 0 deletions samples/Example.Ssl/SslConnection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using ProtoSocket;
using ProtoSocket.Upgraders;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace Example.Ssl
{
public class SslConnection : ProtocolConnection<SslConnection, string>
{
protected async override void OnConnected(PeerConnectedEventArgs<string> e)
{
// call connected, we can't upgrade until the peer has been marked as connected
base.OnConnected(e);

// upgrade, if an error occurs log
try {
SslUpgrader upgrader = new SslUpgrader(((SslServer)Server).Certificate);

await UpgradeAsync(upgrader);
} catch(Exception ex) {
Console.Error.WriteLine($"err:{e.Peer.RemoteEndPoint}: failed to upgrade SSL: {ex.ToString()}");
return;
}

// enable active mode so frames start being read by ProtoSocket
Mode = ProtocolMode.Active;
}

protected override bool OnReceived(PeerReceivedEventArgs<string> e)
{
// log message
Console.WriteLine($"msg:{e.Peer.RemoteEndPoint}: {e.Frame}");

// send an empty frame reply, we send as a fire and forget for the purposes of simplicity
// any exception will be lost to the ether
Task _ = SendAsync(string.Empty);

// indicates that we observed this frame, it will still call Notify/etc and other handlers but it won't add to the receive queue
return true;
}

public SslConnection(ProtocolServer<SslConnection, string> server, ProtocolCoderFactory<string> coderFactory, PeerConfiguration configuration = null) : base(server, coderFactory, configuration) {
}
}
}
23 changes: 23 additions & 0 deletions samples/Example.Ssl/SslServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using ProtoSocket;
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace Example.Ssl
{
public class SslServer : ProtocolServer<SslConnection, string>
{
private X509Certificate2 _cert;

internal X509Certificate2 Certificate {
get {
return _cert;
}
}

public SslServer(X509Certificate2 cert) : base(p => new SslCoder(), new PeerConfiguration(ProtocolMode.Passive)) {
_cert = cert;
}
}
}
Loading

0 comments on commit 2fd95d1

Please sign in to comment.