Skip to content

Commit 2fd95d1

Browse files
committed
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
1 parent 39667ca commit 2fd95d1

File tree

14 files changed

+388
-42
lines changed

14 files changed

+388
-42
lines changed

ProtoSocket.sln

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.Chat", "samples\Exa
1616
EndProject
1717
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.Minecraft", "samples\Example.Minecraft\Example.Minecraft.csproj", "{D9E913D0-B075-489A-96AE-7389C86C790E}"
1818
EndProject
19-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.Line", "samples\Example.Line\Example.Line.csproj", "{5C1CEDDA-43A7-4501-BF28-E4A8BC142574}"
19+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example.Line", "samples\Example.Line\Example.Line.csproj", "{5C1CEDDA-43A7-4501-BF28-E4A8BC142574}"
20+
EndProject
21+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.Ssl", "samples\Example.Ssl\Example.Ssl.csproj", "{8387CAE0-ADDC-4B06-BB97-17190D788EFE}"
2022
EndProject
2123
Global
2224
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -40,6 +42,10 @@ Global
4042
{5C1CEDDA-43A7-4501-BF28-E4A8BC142574}.Debug|Any CPU.Build.0 = Debug|Any CPU
4143
{5C1CEDDA-43A7-4501-BF28-E4A8BC142574}.Release|Any CPU.ActiveCfg = Release|Any CPU
4244
{5C1CEDDA-43A7-4501-BF28-E4A8BC142574}.Release|Any CPU.Build.0 = Release|Any CPU
45+
{8387CAE0-ADDC-4B06-BB97-17190D788EFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
46+
{8387CAE0-ADDC-4B06-BB97-17190D788EFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
47+
{8387CAE0-ADDC-4B06-BB97-17190D788EFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
48+
{8387CAE0-ADDC-4B06-BB97-17190D788EFE}.Release|Any CPU.Build.0 = Release|Any CPU
4349
EndGlobalSection
4450
GlobalSection(SolutionProperties) = preSolution
4551
HideSolutionNode = FALSE
@@ -48,6 +54,7 @@ Global
4854
{3244FC3C-5703-4B56-A754-CC6D7DB87039} = {232DEEE3-098B-46F0-803A-591781CEB430}
4955
{D9E913D0-B075-489A-96AE-7389C86C790E} = {232DEEE3-098B-46F0-803A-591781CEB430}
5056
{5C1CEDDA-43A7-4501-BF28-E4A8BC142574} = {232DEEE3-098B-46F0-803A-591781CEB430}
57+
{8387CAE0-ADDC-4B06-BB97-17190D788EFE} = {232DEEE3-098B-46F0-803A-591781CEB430}
5158
EndGlobalSection
5259
GlobalSection(ExtensibilityGlobals) = postSolution
5360
SolutionGuid = {056A9443-6614-4870-A4BA-EE86AF7BF8C8}

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ await client.UpgradeAsync(upgrader);
131131
client.Mode = ProtocolMode.Active;
132132
```
133133

134+
You can find an example of using SSL [here](samples/Example.Ssl).
135+
134136
### Statistics
135137

136138
In the newer versions of ProtoSocket you can request network statistics from the peer without dynamic allocation.

docs/img/example_ssl.png

13.9 KB
Loading
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>netcoreapp2.2</TargetFramework>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectReference Include="..\..\src\ProtoSocket\ProtoSocket.csproj" />
10+
</ItemGroup>
11+
12+
</Project>

samples/Example.Ssl/Program.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.Security.Cryptography.X509Certificates;
3+
using System.Threading.Tasks;
4+
5+
namespace Example.Ssl
6+
{
7+
class Program
8+
{
9+
static Task Main(string[] args)
10+
{
11+
Server();
12+
return ClientAsync();
13+
}
14+
15+
static void Server()
16+
{
17+
// configure the server
18+
SslServer server = new SslServer(new X509Certificate2("ssl.p12"));
19+
server.Configure("tcp://127.0.0.1:3001");
20+
21+
server.Connected += (o, e) => {
22+
Console.WriteLine($"srv:{e.Peer.RemoteEndPoint}: connected");
23+
};
24+
25+
server.Disconnected += (o, e) => {
26+
Console.WriteLine($"srv:{e.Peer.RemoteEndPoint}: disconnected");
27+
};
28+
29+
// start the server
30+
server.Start();
31+
}
32+
33+
static async Task ClientAsync()
34+
{
35+
// try and connect three times, on the third time we will show an error
36+
SslClient client = null;
37+
38+
for (int i = 0; i < 3; i++) {
39+
client = new SslClient();
40+
41+
try {
42+
await client.ConnectAsync(new Uri("tcp://127.0.0.1:3001"))
43+
.ConfigureAwait(false);
44+
break;
45+
} catch(Exception ex) {
46+
if (i == 2) {
47+
Console.Error.WriteLine($"client:{ex.ToString()}");
48+
return;
49+
} else {
50+
await Task.Delay(1000)
51+
.ConfigureAwait(false);
52+
}
53+
}
54+
}
55+
56+
// show a basic read line prompt, sending every frame to the server once enter is pressed
57+
string line = null;
58+
59+
do {
60+
// read a line of data
61+
Console.Write("> ");
62+
line = (await Console.In.ReadLineAsync().ConfigureAwait(false)).Trim();
63+
64+
// send
65+
await client.SendAsync(line)
66+
.ConfigureAwait(false);
67+
68+
// wait for reply
69+
await client.ReceiveAsync()
70+
.ConfigureAwait(false);
71+
} while (!line.Equals("exit", StringComparison.OrdinalIgnoreCase));
72+
}
73+
}
74+
}

samples/Example.Ssl/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# SSL Example
2+
3+
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.
4+
5+
The example also provides a good `IProtocolCoder` implementation which can be extended to support any length-prefixed frame based protocol.
6+
7+
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.
8+
9+
### Generating a certificate
10+
11+
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.
12+
13+
```
14+
openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out ssl.pem
15+
openssl pkcs12 -inkey key.pem -in ssl.pem -export -out ssl.p12
16+
```
17+
18+
#### Notes
19+
20+
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.
21+
22+
## Usage
23+
24+
Once a valid certificate has been generated you can type chat messages to the server, which will print them out.
25+
26+
![Example Ssl](../../docs/img/example_ssl.png)

samples/Example.Ssl/SslClient.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using ProtoSocket;
2+
using ProtoSocket.Upgraders;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
8+
namespace Example.Ssl
9+
{
10+
public class SslClient : ProtocolClient<string>
11+
{
12+
public override async Task ConnectAsync(Uri uri)
13+
{
14+
// connect to the server
15+
await base.ConnectAsync(uri).ConfigureAwait(false);
16+
17+
// upgrade, this isn't setup to verify trust correctly and will blindly accept any certificate
18+
// DO NOT USE IN PRODUCTION
19+
try {
20+
SslUpgrader upgrader = new SslUpgrader(uri.Host);
21+
upgrader.RemoteValidationCallback = (o, crt, cert, sse) => {
22+
return true;
23+
};
24+
25+
await UpgradeAsync(upgrader).ConfigureAwait(false);
26+
} catch(Exception) {
27+
Dispose();
28+
throw;
29+
}
30+
31+
// enable active mode so frames start being read by ProtoSocket
32+
Mode = ProtocolMode.Active;
33+
}
34+
35+
public SslClient() : base(new SslCoder(), new PeerConfiguration(ProtocolMode.Passive)) {
36+
}
37+
}
38+
}

samples/Example.Ssl/SslCoder.cs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
using ProtoSocket;
2+
using System;
3+
using System.Buffers;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.IO.Pipelines;
7+
using System.Text;
8+
9+
namespace Example.Ssl
10+
{
11+
public class SslCoder : IProtocolCoder<string>
12+
{
13+
private byte[] _bytes;
14+
private int _bytesLength;
15+
private int _bytesOffset;
16+
private State _state;
17+
18+
public bool Read(PipeReader reader, CoderContext<string> ctx, out string frame)
19+
{
20+
if (reader.TryRead(out ReadResult result) && !result.IsCompleted) {
21+
// get the sequence buffer
22+
ReadOnlySequence<byte> buffer = result.Buffer;
23+
24+
try {
25+
while (buffer.Length > 0) {
26+
if (_state == State.Size) {
27+
if (buffer.Length >= 2) {
28+
// copy length from buffer
29+
Span<byte> lengthBytes = stackalloc byte[2];
30+
31+
buffer.Slice(0, 2)
32+
.CopyTo(lengthBytes);
33+
34+
int length = BitConverter.ToUInt16(lengthBytes);
35+
36+
if (length > 32768)
37+
throw new ProtocolCoderException("The client sent an invalid frame length");
38+
39+
// increment the amount we were able to copy in
40+
buffer = buffer.Slice(2);
41+
42+
// move state to content if we have a message with data
43+
if (length > 0) {
44+
_bytes = ArrayPool<byte>.Shared.Rent(length);
45+
_bytesLength = length;
46+
_bytesOffset = 0;
47+
_state = State.Content;
48+
} else {
49+
frame = string.Empty;
50+
return true;
51+
}
52+
} else {
53+
break;
54+
}
55+
} else if (_state == State.Content) {
56+
if (buffer.Length >= 1) {
57+
// figure out how much data we can read, and how much we actually have to read
58+
int remainingBytes = _bytesLength - _bytesOffset;
59+
int maxBytes = Math.Min((int)buffer.Length, remainingBytes);
60+
61+
// copy into buffer
62+
buffer.Slice(0, maxBytes)
63+
.CopyTo(_bytes.AsSpan(_bytesOffset, maxBytes));
64+
65+
// increment offset by amount we copied
66+
_bytesOffset += maxBytes;
67+
buffer = buffer.Slice(maxBytes);
68+
69+
// if we have filled the content array we can now produce a frame
70+
if (_bytesOffset == _bytesLength) {
71+
try {
72+
frame = Encoding.UTF8.GetString(_bytes);
73+
_state = State.Size;
74+
return true;
75+
} finally {
76+
ArrayPool<byte>.Shared.Return(_bytes);
77+
_bytes = null;
78+
}
79+
}
80+
}
81+
}
82+
}
83+
} finally {
84+
reader.AdvanceTo(buffer.GetPosition(0), buffer.End);
85+
}
86+
}
87+
88+
// we didn't find a frame
89+
frame = default;
90+
return false;
91+
}
92+
93+
public void Reset()
94+
{
95+
throw new NotSupportedException();
96+
}
97+
98+
public void Write(Stream stream, string frame, CoderContext<string> ctx)
99+
{
100+
// encode the frame into a UTF8 byte array
101+
byte[] frameBytes = Encoding.UTF8.GetBytes(frame);
102+
103+
if (frameBytes.Length > 32768)
104+
throw new ProtocolCoderException("The frame is too large to write");
105+
106+
// encode the length
107+
byte[] lengthBytes = BitConverter.GetBytes((ushort)frame.Length);
108+
109+
// write to stream
110+
stream.Write(lengthBytes);
111+
stream.Write(frameBytes);
112+
}
113+
114+
/// <summary>
115+
/// Defines the states for this coder.
116+
/// </summary>
117+
enum State
118+
{
119+
Size,
120+
Content
121+
}
122+
123+
~SslCoder()
124+
{
125+
// return the array back to the pool if we deconstruct before finishing the entire frame
126+
if (_bytes != null)
127+
ArrayPool<byte>.Shared.Return(_bytes);
128+
}
129+
}
130+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using ProtoSocket;
2+
using ProtoSocket.Upgraders;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
8+
namespace Example.Ssl
9+
{
10+
public class SslConnection : ProtocolConnection<SslConnection, string>
11+
{
12+
protected async override void OnConnected(PeerConnectedEventArgs<string> e)
13+
{
14+
// call connected, we can't upgrade until the peer has been marked as connected
15+
base.OnConnected(e);
16+
17+
// upgrade, if an error occurs log
18+
try {
19+
SslUpgrader upgrader = new SslUpgrader(((SslServer)Server).Certificate);
20+
21+
await UpgradeAsync(upgrader);
22+
} catch(Exception ex) {
23+
Console.Error.WriteLine($"err:{e.Peer.RemoteEndPoint}: failed to upgrade SSL: {ex.ToString()}");
24+
return;
25+
}
26+
27+
// enable active mode so frames start being read by ProtoSocket
28+
Mode = ProtocolMode.Active;
29+
}
30+
31+
protected override bool OnReceived(PeerReceivedEventArgs<string> e)
32+
{
33+
// log message
34+
Console.WriteLine($"msg:{e.Peer.RemoteEndPoint}: {e.Frame}");
35+
36+
// send an empty frame reply, we send as a fire and forget for the purposes of simplicity
37+
// any exception will be lost to the ether
38+
Task _ = SendAsync(string.Empty);
39+
40+
// indicates that we observed this frame, it will still call Notify/etc and other handlers but it won't add to the receive queue
41+
return true;
42+
}
43+
44+
public SslConnection(ProtocolServer<SslConnection, string> server, ProtocolCoderFactory<string> coderFactory, PeerConfiguration configuration = null) : base(server, coderFactory, configuration) {
45+
}
46+
}
47+
}

samples/Example.Ssl/SslServer.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using ProtoSocket;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Security.Cryptography.X509Certificates;
5+
using System.Text;
6+
7+
namespace Example.Ssl
8+
{
9+
public class SslServer : ProtocolServer<SslConnection, string>
10+
{
11+
private X509Certificate2 _cert;
12+
13+
internal X509Certificate2 Certificate {
14+
get {
15+
return _cert;
16+
}
17+
}
18+
19+
public SslServer(X509Certificate2 cert) : base(p => new SslCoder(), new PeerConfiguration(ProtocolMode.Passive)) {
20+
_cert = cert;
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)