Skip to content

Commit

Permalink
#585: initial work to support client web sockets
Browse files Browse the repository at this point in the history
  • Loading branch information
Badgerati committed Aug 30, 2020
1 parent 9f8e6a7 commit c8a5536
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 42 deletions.
20 changes: 7 additions & 13 deletions examples/public/scripts/websockets.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
$(document).ready(() => {
// bind submit on the form to send message to the server
$('#bc-form').submit(function(e) {
e.preventDefault();

$.ajax({
url: '/broadcast',
type: 'post',
data: $('#bc-form').serialize()
})

$('input[name=message]').val('')
})

// create the websocket
var ws = new WebSocket("ws://localhost:8091/");

Expand All @@ -20,4 +7,11 @@ $(document).ready(() => {
var data = JSON.parse(evt.data)
$('#messages').append(`<p>${data.Message}</p>`);
}

// send message on the socket
$('#bc-form').submit(function(e) {
e.preventDefault();
ws.send($('#bc-message').val());
$('input[name=message]').val('')
})
})
2 changes: 1 addition & 1 deletion examples/views/websockets.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<h1>Example of using a WebSockets</h1>
<p>Clicking submit will broadcast the message to all connected clients - try opening this page on multiple, and different, browsers!</p>
<form id='bc-form'>
<input type='text' name='message' placeholder='Enter any random text' />
<input type='text' name='message' id='bc-message' placeholder='Enter any random text' />
<input type='submit' value='Broadcast!' />
</form>

Expand Down
18 changes: 18 additions & 0 deletions src/Listener/PodeClientSignal.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;

namespace Pode
{
public class PodeClientSignal
{
public PodeWebSocket WebSocket { get; private set; }
public string Message { get; private set; }
public DateTime Timestamp { get; private set; }

public PodeClientSignal(PodeWebSocket webSocket, string message)
{
WebSocket = webSocket;
Message = message;
Timestamp = DateTime.UtcNow;
}
}
}
27 changes: 22 additions & 5 deletions src/Listener/PodeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,21 @@ public bool IsWebSocket
get => (Type == PodeContextType.WebSocket);
}

public bool IsWebSocketUpgraded
{
get => (IsWebSocket && HttpRequest.WebSocket != default(PodeWebSocket));
}

public bool IsSmtp
{
get => (Type == PodeContextType.Smtp);
}

public bool IsHttp
{
get => (Type == PodeContextType.Http);
}

public PodeSmtpRequest SmtpRequest
{
get => (PodeSmtpRequest)Request;
Expand Down Expand Up @@ -134,15 +144,20 @@ private void SetContextType()
case PodeListenerType.WebSocket:
if (!HttpRequest.IsWebSocket)
{
throw new HttpRequestException("Request is not for a websocket");
throw new HttpRequestException("Request is not for a WebSocket");
}

Type = PodeContextType.WebSocket;
break;

// - only allow http, with upgrade to web-socket
// - only allow http
case PodeListenerType.Http:
Type = HttpRequest.IsWebSocket ? PodeContextType.WebSocket : PodeContextType.Http;
if (HttpRequest.IsWebSocket)
{
throw new HttpRequestException("Request is not Http");
}

Type = PodeContextType.Http;
break;
}
}
Expand Down Expand Up @@ -209,7 +224,8 @@ public void UpgradeWebSocket(string clientId = null)
Response.Send();

// add open web socket to listener
Listener.AddWebSocket(new PodeWebSocket(this, HttpRequest.Url.AbsolutePath, clientId));
HttpRequest.WebSocket = new PodeWebSocket(this, HttpRequest.Url.AbsolutePath, clientId);
Listener.AddWebSocket(HttpRequest.WebSocket);
}

public void Dispose()
Expand All @@ -227,7 +243,8 @@ public void Dispose(bool force)
Response.StatusCode = 500;
}

if (!IsSmtp && State != PodeContextState.SslError)
// only send a response if Http
if (IsHttp && State != PodeContextState.SslError)
{
Response.Send();
}
Expand Down
81 changes: 75 additions & 6 deletions src/Listener/PodeHttpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ public class PodeHttpRequest : PodeRequest
public string Body { get; private set; }
public byte[] RawBody { get; private set; }
public string Host { get; private set; }
public bool IsWebSocket { get; private set; }
public PodeWebSocket WebSocket { get; set; }

private bool _isWebSocket = false;
public bool IsWebSocket
{
get => (_isWebSocket || WebSocket != default(PodeWebSocket));
}

public override bool CloseImmediately
{
Expand All @@ -45,6 +51,61 @@ public PodeHttpRequest(Socket socket)
Protocol = "HTTP/1.1";
}

public PodeClientSignal NewClientSignal()
{
return new PodeClientSignal(WebSocket, Body);
}

protected void ParseWebSocket(byte[] bytes)
{
var dataLength = bytes[1] - 128;
var offset = 0;
//var totalLength = 0;

if (dataLength < 126)
{
offset = 2;
//totalLength = dataLength + 6;
}
else if (dataLength == 126)
{
dataLength = BitConverter.ToInt16(new byte[] { bytes[3], bytes[2] }, 0);
offset = 4;
//totalLength = dataLength + 8;
}
else
{
dataLength = (int)BitConverter.ToInt64(new byte[] { bytes[9], bytes[8], bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2] }, 0);
offset = 10;
//totalLength = dataLength + 14;
}

var key = new byte[] { bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3] };
offset += 4;

//var count = 0;
var decoded = new byte[dataLength];
// for (var i = offset; i < totalLength; i++)
// {
// bytes[i] = (byte)(bytes[i] ^ key[count % 4]);
// count++;
// }
for (var i = 0; i < dataLength; ++i)
{
decoded[i] = (byte)(bytes[offset + i] ^ key[i % 4]);
}

RawBody = bytes;
//Body = Encoding.GetString(bytes, offset, dataLength);
Body = Encoding.GetString(decoded);
Console.WriteLine(Body);


//TODO: here, or in a PodeWsRequest class?
// https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_server
// https://stackoverflow.com/questions/10200910/creating-a-hello-world-websocket-example
}

protected override void Parse(byte[] bytes)
{
// if there are no bytes, return (0 bytes read means we can close the socket)
Expand All @@ -54,6 +115,13 @@ protected override void Parse(byte[] bytes)
return;
}

// check if websocket, and parse
if (IsWebSocket)
{
ParseWebSocket(bytes);
return;
}

// get the raw string for headers
var content = Encoding.GetString(bytes, 0, bytes.Length);

Expand Down Expand Up @@ -201,12 +269,13 @@ protected override void Parse(byte[] bytes)
UserAgent = $"{Headers["User-Agent"]}";
ContentType = $"{Headers["Content-Type"]}";

// keep-alive?
IsKeepAlive = (Headers.ContainsKey("Connection")
&& $"{Headers["Connection"]}".Equals("keep-alive", StringComparison.InvariantCultureIgnoreCase));

// is web-socket?
IsWebSocket = Headers.ContainsKey("Sec-WebSocket-Key");
_isWebSocket = Headers.ContainsKey("Sec-WebSocket-Key");

// keep-alive?
IsKeepAlive = (_isWebSocket ||
(Headers.ContainsKey("Connection")
&& $"{Headers["Connection"]}".Equals("keep-alive", StringComparison.InvariantCultureIgnoreCase)));

// set content encoding
ContentEncoding = System.Text.Encoding.UTF8;
Expand Down
38 changes: 29 additions & 9 deletions src/Listener/PodeListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public class PodeListener : IDisposable

private IList<PodeSocket> Sockets;
private BlockingCollection<PodeContext> Contexts;
private BlockingCollection<PodeSignal> Signals;
private BlockingCollection<PodeServerSignal> ServerSignals;
private BlockingCollection<PodeClientSignal> ClientSignals;

public PodeListener(CancellationToken cancellationToken, PodeListenerType type = PodeListenerType.Http)
{
Expand All @@ -26,7 +27,8 @@ public PodeListener(CancellationToken cancellationToken, PodeListenerType type =
Sockets = new List<PodeSocket>();
WebSockets = new Dictionary<string, PodeWebSocket>();
Contexts = new BlockingCollection<PodeContext>();
Signals = new BlockingCollection<PodeSignal>();
ServerSignals = new BlockingCollection<PodeServerSignal>();
ClientSignals = new BlockingCollection<PodeClientSignal>();
}

public void Add(PodeSocket socket)
Expand Down Expand Up @@ -68,21 +70,39 @@ public void AddWebSocket(PodeWebSocket webSocket)
}
}

public PodeSignal GetSignal(CancellationToken cancellationToken)
public PodeServerSignal GetServerSignal(CancellationToken cancellationToken)
{
return Signals.Take(cancellationToken);
return ServerSignals.Take(cancellationToken);
}

public Task<PodeSignal> GetSignalAsync(CancellationToken cancellationToken)
public Task<PodeServerSignal> GetServerSignalAsync(CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => GetSignal(cancellationToken), cancellationToken);
return Task.Factory.StartNew(() => GetServerSignal(cancellationToken), cancellationToken);
}

public void AddSignal(string value, string path, string clientId)
public void AddServerSignal(string value, string path, string clientId)
{
lock (Signals)
lock (ServerSignals)
{
Signals.Add(new PodeSignal(value, path, clientId));
ServerSignals.Add(new PodeServerSignal(value, path, clientId));
}
}

public PodeClientSignal GetClientSignal(CancellationToken cancellationToken)
{
return ClientSignals.Take(cancellationToken);
}

public Task<PodeClientSignal> GetClientSignalAsync(CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => GetClientSignal(cancellationToken), cancellationToken);
}

public void AddClientSignal(PodeClientSignal signal)
{
lock (ClientSignals)
{
ClientSignals.Add(signal);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Listener/PodeResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public void Send()
}
}

public void SendSignal(PodeSignal signal)
public void SendSignal(PodeServerSignal signal)
{
Write(signal.Value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

namespace Pode
{
public class PodeSignal
public class PodeServerSignal
{
public string Value { get; private set; }
public string Path { get; private set; }
public string ClientId { get; private set; }
public DateTime Timestamp { get; private set; }

public PodeSignal(string value, string path, string clientId)
public PodeServerSignal(string value, string path, string clientId)
{
Value = value;
Path = path;
Expand Down
15 changes: 12 additions & 3 deletions src/Listener/PodeSocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,12 @@ private void ProcessReceive(SocketAsyncEventArgs args)
process = false;
}

// if it's a websocket, upgrade it
else if (context.IsWebSocket)
// if it's a websocket, upgrade it, then add context back for re-receiving
else if (context.IsWebSocket && !context.IsWebSocketUpgraded)
{
context.UpgradeWebSocket();
process = false;
context.Dispose();
}

// if it's an email, re-receive unless processable
Expand All @@ -234,7 +235,15 @@ private void ProcessReceive(SocketAsyncEventArgs args)
// add the context for processing
if (process)
{
Listener.AddContext(context);
if (context.IsWebSocket)
{
Listener.AddClientSignal(context.HttpRequest.NewClientSignal());
context.Dispose();
}
else
{
Listener.AddContext(context);
}
}
}
catch (Exception ex)
Expand Down
2 changes: 1 addition & 1 deletion src/Private/SignalServer.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function Start-PodeSignalServer
try {
while ($Listener.IsListening -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested)
{
$message = (Wait-PodeTask -Task $Listener.GetSignalAsync($PodeContext.Tokens.Cancellation.Token))
$message = (Wait-PodeTask -Task $Listener.GetServerSignalAsync($PodeContext.Tokens.Cancellation.Token))

# get the sockets for the message
$sockets = @()
Expand Down
2 changes: 1 addition & 1 deletion src/Public/Responses.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1340,5 +1340,5 @@ function Send-PodeSignal
}
}

$PodeContext.Server.WebSockets.Listener.AddSignal($Value, $Path, $ClientId)
$PodeContext.Server.WebSockets.Listener.AddServerSignal($Value, $Path, $ClientId)
}

0 comments on commit c8a5536

Please sign in to comment.