Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for client-to-server websockets #605

Merged
merged 3 commits into from
Sep 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 30 additions & 27 deletions docs/Tutorials/WebSockets.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
# Web Sockets

Pode has support for server-to-client communications using WebSockets, including secure WebSockets.

!!! note
Currently only broadcasting messages to connected clients/browsers from the server is supported. Client-to-server communications is in the works!
Pode has support for using WebSockets, including secure WebSockets, for either server-to-client or vice-versa.

WebSockets allow you to send messages directly from your server to connected clients. This allows you to get real-time continuous updates for the frontend without having to constantly refresh the page, or by using async javascript!

## Server Side

### Listening

On the server side, the only real work required is to register a new endpoint to listen on. To do this you can use the normal [`Add-PodeEndpoint`](../../Functions/Core/Add-PodeEndpoint) function, but with a protocol of either `Ws` or `Wss`:
On the server side, the only real work required is to register a new endpoint to listen on. To do this you can use the normal [`Add-PodeEndpoint`](../../Functions/Core/Add-PodeEndpoint), but with a protocol of either `Ws` or `Wss`:

```powershell
Add-PodeEndpoint -Address * -Port 8091 -Protocol Ws
Expand All @@ -22,7 +19,7 @@ Add-PodeEndpoint -Address * -Port 8091 -Certificate './path/cert.pfx' -Certifica

### Broadcasting

To broadcast a message from the server to all connected clients you can use the [`Send-PodeSignal`](../../Functions/Responses/Send-PodeSignal) function. You can either send raw JSON data, or you can pass a HashTable/PSObject and it will be converted to JSON for you.
To broadcast a message from the server to all connected clients you can use [`Send-PodeSignal`](../../Functions/Responses/Send-PodeSignal). You can either send raw JSON data, or you can pass a HashTable/PSObject and it will be converted to JSON for you.

To broadcast some data to all clients from a POST route, you could use the following. This will get some message from one of the clients, and then broadcast it to every other client:

Expand All @@ -45,6 +42,8 @@ You can also broadcast messages from Timers, or from Schedules.

## Client Side

### Receiving

On the client side, you need to use javascript to register a WebSocket and then bind the `onmessage` event to do something when a broadcasted message is received.

To create a WebSocket, you can do something like the following which will bind a WebSocket onto the root path '/':
Expand All @@ -62,6 +61,22 @@ $(document).ready(() => {
})
```

### Sending

To send a message using the WebSocket, you can use the `.send` function. When you send a message from client-to-server, the data must be a JSON value containing the `message`, `path`, and `clientId`. Only the `message` is mandatory.

For example, if you have a form with input, you can send the message as follows:

```javascript
$('#form').submit(function(e) {
e.preventDefault();
ws.send(JSON.stringify({ message: $('#input').val() }));
$('#input').val('');
})
```

This will send the message to the server, which will in-turn broadcast to all other clients.

## Full Example

> This full example is a cut-down version of the one found in `/examples/web-signal.ps1` of the main repository.
Expand All @@ -78,10 +93,7 @@ server.ps1
script.js
```

The following is the Pode server code, that will create two routes.

* The first route will be for some home page, with a button/input for broadcasting messages.
* The second route will be invoked when the button above is clicked. It will then broadcast some message to all clients.
The following is the Pode server code, that will create one route, which will be for some home page, with a button/input for broadcasting messages.

```powershell
Start-PodeServer {
Expand All @@ -94,12 +106,6 @@ Start-PodeServer {
Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
Write-PodeViewResponse -Path 'index'
}

# broadcast a received message back out to ever connected client via websockets
Add-PodeRoute -Method Post -Path '/broadcast' -ScriptBlock {
param($e)
Send-PodeSignal -Value @{ Message = $e.Data['message'] }
}
}
```

Expand All @@ -124,30 +130,27 @@ Next we have the HTML web page with a basic button/input for broadcasting messag
</html>
```

Finally, the following is the client-side javascript to register a WebSocket for the client. It will also invoke the `/broadcast` endpoint when the button is clicked:
Finally, the following is the client-side javascript to register a WebSocket for the client. It will also invoke the `.send` function of the WebSocket when the button is clicked:

```javascript
$(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()
})
ws.send(JSON.stringify({
message: $('input[name=message]').val()
}));

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

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

// event for inbound messages to append them
ws.onmessage = function(evt) {
var data = JSON.parse(evt.data)
$('#messages').append(`<p>${data.Message}</p>`);
$('#messages').append(`<p>${evt.data}</p>`);
}
})
});
```
25 changes: 10 additions & 15 deletions examples/public/scripts/websockets.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
$(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/");

// event for inbound messages to append them
ws.onmessage = function(evt) {
var data = JSON.parse(evt.data)
$('#messages').append(`<p>${data.Message}</p>`);
//var data = JSON.parse(evt.data)
console.log(evt.data);
$('#messages').append(`<p>${evt.data}</p>`);
}

// send message on the socket, to all clients
$('#bc-form').submit(function(e) {
e.preventDefault();
ws.send(JSON.stringify({ message: $('#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
6 changes: 0 additions & 6 deletions examples/web-signal.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,4 @@ Start-PodeServer -Threads 5 {
Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
Write-PodeViewResponse -Path 'websockets'
}

# POST broadcast a received message back out to ever connected client via websockets
Add-PodeRoute -Method Post -Path '/broadcast' -ScriptBlock {
param($e)
Send-PodeSignal -Value @{ Message = $e.Data['message'] }
}
}
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;
}
}
}
44 changes: 37 additions & 7 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 && Request is PodeWsRequest);
}

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

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

public PodeSmtpRequest SmtpRequest
{
get => (PodeSmtpRequest)Request;
Expand All @@ -45,6 +55,11 @@ public PodeHttpRequest HttpRequest
get => (PodeHttpRequest)Request;
}

public PodeWsRequest WsRequest
{
get => (PodeWsRequest)Request;
}

public bool IsKeepAlive
{
get => (Request.IsKeepAlive);
Expand Down Expand Up @@ -134,15 +149,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 +229,16 @@ public void UpgradeWebSocket(string clientId = null)
Response.Send();

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

var wsRequest = new PodeWsRequest(HttpRequest);
wsRequest.WebSocket = webSocket;
Request = wsRequest;

Listener.AddWebSocket(WsRequest.WebSocket);

// HttpRequest.WebSocket = new PodeWebSocket(this, HttpRequest.Url.AbsolutePath, clientId);
// Listener.AddWebSocket(HttpRequest.WebSocket);
}

public void Dispose()
Expand All @@ -227,7 +256,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 All @@ -238,13 +268,13 @@ public void Dispose(bool force)
SmtpRequest.Reset();
}

Response.Dispose();

if (!IsKeepAlive || force)
{
State = PodeContextState.Closed;
Request.Dispose();
}

Response.Dispose();
}
catch {}

Expand Down
23 changes: 12 additions & 11 deletions src/Listener/PodeHttpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
Expand All @@ -31,7 +26,12 @@ 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; }

private bool _isWebSocket = false;
public bool IsWebSocket
{
get => _isWebSocket;
}

public override bool CloseImmediately
{
Expand Down Expand Up @@ -201,12 +201,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
Loading