-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added SSL example, bug fixes and upgrade refactor
- 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
1 parent
39667ca
commit 2fd95d1
Showing
14 changed files
with
388 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) { | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.