Skip to content

Commit 7362a81

Browse files
committed
Add four example programs demonstrating mare features
The examples directory only had the echo example. Add four more covering features not yet demonstrated: request filtering (on_upgrade_request), close code handling (server-initiated close and CloseStatus matching), inter-actor communication (multi-client broadcast chat), and WSS (TLS with self-signed certificates). Each example follows the same actor pattern as echo (Main, Listener, Handler) and uses a unique port to allow running multiple simultaneously. Self-signed test certificates copied from lori for the WSS example.
1 parent ecbe10f commit 7362a81

7 files changed

Lines changed: 458 additions & 2 deletions

File tree

assets/cert.pem

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDLzCCAhegAwIBAgIULwCZKF2RnWxksm81UIxw2oRUYUswDQYJKoZIhvcNAQEL
3+
BQAwGTEXMBUGA1UECgwORmFrZSBBdXRob3JpdHkwHhcNMjYwMjEwMDU0MzAzWhcN
4+
MzYwMjA4MDU0MzAzWjAZMRcwFQYDVQQKDA5GYWtlIEF1dGhvcml0eTCCASIwDQYJ
5+
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAL+K+Mp6NlKQUL/2H6LdoctJpPYu4rj1
6+
vFdoq9EeXE+fllOjM35ofsvEJlekd6TPAGCeADrDquWbWvJ98Aw3tjgQK0Z45KI2
7+
wQTQ0tA5JsR94ocjLQwuRP2yW1Ul8K1+OyMlkHlWoVU82H/IvcZGTPv8emr97jJg
8+
kDT9RxxzMnsXGEi2UJtU4sw0c6kdAqDej0tAiShkJKtNuvTDwMMdSTiKiy2YMbdb
9+
O2ov8mQ0uPJc+4pI2ijl3NBynxteHLGb4/REyuwoCcAS4z+efzB5TYaL1fKXsNEx
10+
CuB9q8da2MbZnMeX/8POW3ZfMTNDycLkLvXgZTTZGOEFqx1QSKDp/kcCAwEAAaNv
11+
MG0wHQYDVR0OBBYEFHBJNj4jFE+i8q491cjm4ajpnyVMMB8GA1UdIwQYMBaAFHBJ
12+
Nj4jFE+i8q491cjm4ajpnyVMMA8GA1UdEwEB/wQFMAMBAf8wGgYDVR0RBBMwEYIJ
13+
bG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQAH5djzuh1tYpJgGqSZ
14+
IKavvrq0A5CtuRCzpakzI/kolOenvCvzipM3trpafTyEqvGhq1Bdlks2IM400aaa
15+
hztPC0em/J0O7IOht0T0rVcHfKdeFxIlr/oF22KVUyFaXXDXPqTNxsmDO9HNfznC
16+
dZO8/mdH0jvd4kq9991l7oA5Iw+9BB2rtOE0El5i+Zkxj0hIEAsbuyBT4kgxga3l
17+
rlFMyrRUf4YepKv/SyQrQDBZjdrEGnHaluGH1t6LWR+DPqzSx91XKYMQ4+jwCTKm
18+
UHK75tIjxXGALmX4dBisEQz/eZtEgnztZ2HQua+hO7AkNo26K08qMfLoPwNITAMw
19+
9rRl
20+
-----END CERTIFICATE-----

assets/key.pem

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/ivjKejZSkFC/
3+
9h+i3aHLSaT2LuK49bxXaKvRHlxPn5ZTozN+aH7LxCZXpHekzwBgngA6w6rlm1ry
4+
ffAMN7Y4ECtGeOSiNsEE0NLQOSbEfeKHIy0MLkT9sltVJfCtfjsjJZB5VqFVPNh/
5+
yL3GRkz7/Hpq/e4yYJA0/UccczJ7FxhItlCbVOLMNHOpHQKg3o9LQIkoZCSrTbr0
6+
w8DDHUk4iostmDG3WztqL/JkNLjyXPuKSNoo5dzQcp8bXhyxm+P0RMrsKAnAEuM/
7+
nn8weU2Gi9Xyl7DRMQrgfavHWtjG2ZzHl//Dzlt2XzEzQ8nC5C714GU02RjhBasd
8+
UEig6f5HAgMBAAECggEAIAxrK0m+jBr8f9n5GiGWQDLeyJg4tDTdogxv7ynyzRnJ
9+
J85fP8/RI1v7BGAO6sL976yQIkT587gGRBuhThIGSaQe8YgpzSB3NEyScWeWnnrn
10+
q6/NikO1pHMyx/D0r7q2v186KKs/jEkFeoAGjMXXfLQWcY9ySviQOGWGf/KgaCq+
11+
xdqHSLYSZO9RQ3qw4Umz9YyREgNf9GOOp9mVjMzPUX/EHPd5OTPtbFwZhENvsbut
12+
VI/MZ5hxh6TMd262DZX884ysld+fwePaKGeAY+0OCLlDFUdo/aJhYKd3DX93peS8
13+
e6i/+hnLsBAccKmimA1Jpu63GnV2KIaG5V4QhI1pNQKBgQD+G3jB6w/QlelM0t7I
14+
7sHJRfOQzlXwVA+HeZLNAGW6TxSVZfOCZ7OejxJ0Hdzl0uf/eqpfPP98nrzTdsIS
15+
OqcYV/RcTVzwVL+SRixnjDGH6NJTOksJNdoRGAq5UqNQvsx5Fb5+WJQJ5ibnugXg
16+
N0v08r6t92C/7FIIQlaO8+5V0wKBgQDA+DP+dp7PfTypMZjJn7+19uhDu+ZOlk/Q
17+
iBWR2bRSc04SJsOsD9vAwVo4DakC34D1qJ0dflRJGl5B3IrS1gUdAWA49WDrOpyQ
18+
J3rsAoR+5Q0g7ziPKZzrfpYK+1FIzVzAgwwX0SNf3bWzjRuEiOCqEVFFSNOOr9ao
19+
K0ZZcORpPQKBgQDe7wkl0G2CiVMyVDqivF2FkPpcS7o36/fn2nBBrJyTNPgRDm3y
20+
NbrvXARLnTuE4mEp0duRTLm1AqsVNzaSuaSJhfjb1f+41hFwamJgwi1eWJPV8umL
21+
/hQANUCLUIX5vDs17C7BOENA4Au394taKAlasHE7vTHDj7h0iIbaSgYKgwKBgQC+
22+
iZWVUf03tVpbWfyaQWGjw9vNXdBnkqXWqzhZHlSeAPp6k8orsDD1YDW8GUjj7FrC
23+
ti4E/EMvN5kQgTqWOfssi7o+I5QCUIpdEI6gUVAQmAxhk7QOMDGgmgp/Z234a8aE
24+
1CDxt3CtbRbH5rHgKONlKdpMhj9WqatTu/8nu3/dpQKBgCB/8okq6ZU7Uzvr7kEQ
25+
/SYkrMomr/BnPYD2Ddu5vZCwyDWjoIkzEzbr5OjoHJ9bAv3gLZD2hqD0xBNdPNqk
26+
sVZRpLhxkFznNThcJ0udI8LZyMphXhRtM5SDpzM5AoG5d+Q5RfRU2HthjVv7JEIm
27+
Lcer8weg9YyyLp0canmB5w3c
28+
-----END PRIVATE KEY-----

examples/README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# Examples
22

3-
## echo
3+
Each subdirectory is a self-contained Pony program demonstrating a different part of the mare library. Ordered from simplest to most involved.
44

5-
A WebSocket echo server that echoes back text and binary messages. Listens on `ws://localhost:8080`. Connect with any WebSocket client (e.g., `websocat ws://localhost:8080`) and send messages to see them echoed back.
5+
## [echo](echo/)
6+
7+
A WebSocket echo server that echoes back text and binary messages. Listens on `ws://localhost:8080`. Connect with any WebSocket client (e.g., `websocat ws://localhost:8080`) and send messages to see them echoed back. Start here if you're new to the library.
8+
9+
## [request-filter](request-filter/)
10+
11+
A WebSocket server that filters connections by URI path and Origin header. Demonstrates `on_upgrade_request()` to accept or reject connections before the handshake completes — only connections to `/ws` from origin `http://localhost` are accepted, all others receive 403 Forbidden. Connect with `websocat -H "Origin: http://localhost" ws://localhost:8081/ws`.
12+
13+
## [close-codes](close-codes/)
14+
15+
A WebSocket server that demonstrates server-initiated close and close status handling. Send "goodbye" to trigger a normal close (1000), "kick" to trigger a policy violation close (1008), or any other message to echo it back. The `on_closed` callback matches on `CloseStatus` variants (`CloseCode`, `CloseNoStatusReceived`, `CloseAbnormalClosure`, `OtherCloseCode`) to log how the connection closed. Connect with `websocat ws://localhost:8082`.
16+
17+
## [chat](chat/)
18+
19+
A multi-client chat server that broadcasts messages to all connected clients. Demonstrates inter-actor communication: `ChatHandler` actors register with `ChatListener`, which maintains a set of connected handlers and broadcasts incoming messages to all others. Connect multiple clients with `websocat ws://localhost:8083`.
20+
21+
## [wss](wss/)
22+
23+
A secure WebSocket (WSS) echo server using TLS with self-signed certificates. Demonstrates `WebSocketServer.ssl` for TLS support: creating an `SSLContext`, loading certificate and key files, and passing the context through the listener to each handler. Must be run from the project root so the relative certificate paths resolve correctly. Connect with `websocat -k wss://localhost:8443`.

examples/chat/main.pony

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""
2+
A multi-client chat server that broadcasts messages to all connected clients.
3+
4+
Demonstrates inter-actor communication: `ChatHandler` actors register with
5+
`ChatListener`, which maintains a set of connected handlers. When a client
6+
sends a message, its handler asks the listener to broadcast to all other
7+
handlers.
8+
9+
Connect multiple clients with: `websocat ws://localhost:8083`
10+
"""
11+
use collections = "collections"
12+
use lori = "lori"
13+
use ws = "../../mare"
14+
15+
actor Main
16+
new create(env: Env) =>
17+
let auth = lori.TCPListenAuth(env.root)
18+
let config = ws.WebSocketConfig(where
19+
host' = "localhost",
20+
port' = "8083")
21+
ChatListener(auth, config, env.out)
22+
23+
actor ChatListener is lori.TCPListenerActor
24+
var _tcp_listener: lori.TCPListener = lori.TCPListener.none()
25+
let _server_auth: lori.TCPServerAuth
26+
let _config: ws.WebSocketConfig val
27+
let _out: OutStream
28+
let _handlers: collections.SetIs[ChatHandler tag] = collections.SetIs[ChatHandler tag]
29+
30+
new create(
31+
auth: lori.TCPListenAuth,
32+
config: ws.WebSocketConfig val,
33+
out: OutStream)
34+
=>
35+
_server_auth = lori.TCPServerAuth(auth)
36+
_config = config
37+
_out = out
38+
_tcp_listener = lori.TCPListener(auth, config.host, config.port, this)
39+
40+
fun ref _listener(): lori.TCPListener => _tcp_listener
41+
42+
fun ref _on_accept(fd: U32): ChatHandler =>
43+
ChatHandler(_server_auth, fd, _config, _out, this)
44+
45+
fun ref _on_listening() =>
46+
_out.print("Listening on " + _config.host + ":" + _config.port)
47+
48+
fun ref _on_listen_failure() =>
49+
_out.print("Failed to listen on " + _config.host + ":" + _config.port)
50+
51+
be register(handler: ChatHandler tag) =>
52+
_handlers.set(handler)
53+
_out.print("Client joined (" + _handlers.size().string() + " connected)")
54+
55+
be deregister(handler: ChatHandler tag) =>
56+
_handlers.unset(handler)
57+
_out.print("Client left (" + _handlers.size().string() + " connected)")
58+
59+
be broadcast(sender: ChatHandler tag, data: String val) =>
60+
for handler in _handlers.values() do
61+
if handler isnt sender then
62+
handler.deliver(data)
63+
end
64+
end
65+
66+
actor ChatHandler is ws.WebSocketServerActor
67+
var _ws: ws.WebSocketServer = ws.WebSocketServer.none()
68+
let _out: OutStream
69+
let _listener_tag: ChatListener tag
70+
71+
new create(
72+
auth: lori.TCPServerAuth,
73+
fd: U32,
74+
config: ws.WebSocketConfig val,
75+
out: OutStream,
76+
listener: ChatListener tag)
77+
=>
78+
_out = out
79+
_listener_tag = listener
80+
_ws = ws.WebSocketServer(auth, fd, this, config)
81+
82+
fun ref _websocket(): ws.WebSocketServer => _ws
83+
84+
fun ref on_open(request: ws.UpgradeRequest val) =>
85+
_listener_tag.register(this)
86+
87+
fun ref on_text_message(data: String val) =>
88+
_listener_tag.broadcast(this, data)
89+
90+
fun ref on_closed(
91+
close_status: ws.CloseStatus,
92+
close_reason: String val)
93+
=>
94+
_listener_tag.deregister(this)
95+
96+
be deliver(data: String val) =>
97+
_ws.send_text(data)

examples/close-codes/main.pony

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""
2+
A WebSocket server that demonstrates server-initiated close and close
3+
status handling.
4+
5+
Send "goodbye" to trigger a normal close (1000), "kick" to trigger a
6+
policy violation close (1008), or any other message to echo it back.
7+
The `on_closed` callback matches on `CloseStatus` variants to log how
8+
the connection closed.
9+
10+
Connect with: `websocat ws://localhost:8082`
11+
"""
12+
use lori = "lori"
13+
use ws = "../../mare"
14+
15+
actor Main
16+
new create(env: Env) =>
17+
let auth = lori.TCPListenAuth(env.root)
18+
let config = ws.WebSocketConfig(where
19+
host' = "localhost",
20+
port' = "8082")
21+
CloseListener(auth, config, env.out)
22+
23+
actor CloseListener is lori.TCPListenerActor
24+
var _tcp_listener: lori.TCPListener = lori.TCPListener.none()
25+
let _server_auth: lori.TCPServerAuth
26+
let _config: ws.WebSocketConfig val
27+
let _out: OutStream
28+
29+
new create(
30+
auth: lori.TCPListenAuth,
31+
config: ws.WebSocketConfig val,
32+
out: OutStream)
33+
=>
34+
_server_auth = lori.TCPServerAuth(auth)
35+
_config = config
36+
_out = out
37+
_tcp_listener = lori.TCPListener(auth, config.host, config.port, this)
38+
39+
fun ref _listener(): lori.TCPListener => _tcp_listener
40+
41+
fun ref _on_accept(fd: U32): CloseHandler =>
42+
CloseHandler(_server_auth, fd, _config, _out)
43+
44+
fun ref _on_listening() =>
45+
_out.print("Listening on " + _config.host + ":" + _config.port)
46+
47+
fun ref _on_listen_failure() =>
48+
_out.print("Failed to listen on " + _config.host + ":" + _config.port)
49+
50+
actor CloseHandler is ws.WebSocketServerActor
51+
var _ws: ws.WebSocketServer = ws.WebSocketServer.none()
52+
let _out: OutStream
53+
54+
new create(
55+
auth: lori.TCPServerAuth,
56+
fd: U32,
57+
config: ws.WebSocketConfig val,
58+
out: OutStream)
59+
=>
60+
_out = out
61+
_ws = ws.WebSocketServer(auth, fd, this, config)
62+
63+
fun ref _websocket(): ws.WebSocketServer => _ws
64+
65+
fun ref on_open(request: ws.UpgradeRequest val) =>
66+
_out.print("Client connected")
67+
68+
fun ref on_text_message(data: String val) =>
69+
if data == "goodbye" then
70+
_out.print("Client said goodbye, closing normally")
71+
_ws.close(ws.CloseNormal, "client said goodbye")
72+
elseif data == "kick" then
73+
_out.print("Kicking client")
74+
_ws.close(ws.ClosePolicyViolation, "kicked")
75+
else
76+
_out.print("Echo: " + data)
77+
_ws.send_text(data)
78+
end
79+
80+
fun ref on_closed(
81+
close_status: ws.CloseStatus,
82+
close_reason: String val)
83+
=>
84+
let reason_suffix =
85+
if close_reason.size() > 0 then " (" + close_reason + ")"
86+
else ""
87+
end
88+
89+
match close_status
90+
| let c: ws.CloseCode =>
91+
_out.print("Closed with code: " + c.string() + reason_suffix)
92+
| let _: ws.CloseNoStatusReceived =>
93+
_out.print("Closed without status code")
94+
| let _: ws.CloseAbnormalClosure =>
95+
_out.print("Connection dropped abnormally")
96+
| let c: ws.OtherCloseCode =>
97+
_out.print("Closed with other code: " + c.code().string()
98+
+ reason_suffix)
99+
end

examples/request-filter/main.pony

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""
2+
A WebSocket server that filters connections by URI path and Origin header.
3+
4+
Demonstrates `on_upgrade_request()` to accept or reject connections before
5+
the handshake completes. Only connections to `/ws` from origin
6+
`http://localhost` are accepted; all others receive 403 Forbidden.
7+
8+
Connect with: `websocat -H "Origin: http://localhost" ws://localhost:8081/ws`
9+
Rejected: `websocat ws://localhost:8081/nope`
10+
"""
11+
use lori = "lori"
12+
use ws = "../../mare"
13+
14+
actor Main
15+
new create(env: Env) =>
16+
let auth = lori.TCPListenAuth(env.root)
17+
let config = ws.WebSocketConfig(where
18+
host' = "localhost",
19+
port' = "8081")
20+
FilterListener(auth, config, env.out)
21+
22+
actor FilterListener is lori.TCPListenerActor
23+
var _tcp_listener: lori.TCPListener = lori.TCPListener.none()
24+
let _server_auth: lori.TCPServerAuth
25+
let _config: ws.WebSocketConfig val
26+
let _out: OutStream
27+
28+
new create(
29+
auth: lori.TCPListenAuth,
30+
config: ws.WebSocketConfig val,
31+
out: OutStream)
32+
=>
33+
_server_auth = lori.TCPServerAuth(auth)
34+
_config = config
35+
_out = out
36+
_tcp_listener = lori.TCPListener(auth, config.host, config.port, this)
37+
38+
fun ref _listener(): lori.TCPListener => _tcp_listener
39+
40+
fun ref _on_accept(fd: U32): FilterHandler =>
41+
FilterHandler(_server_auth, fd, _config, _out)
42+
43+
fun ref _on_listening() =>
44+
_out.print("Listening on " + _config.host + ":" + _config.port)
45+
46+
fun ref _on_listen_failure() =>
47+
_out.print("Failed to listen on " + _config.host + ":" + _config.port)
48+
49+
actor FilterHandler is ws.WebSocketServerActor
50+
var _ws: ws.WebSocketServer = ws.WebSocketServer.none()
51+
let _out: OutStream
52+
53+
new create(
54+
auth: lori.TCPServerAuth,
55+
fd: U32,
56+
config: ws.WebSocketConfig val,
57+
out: OutStream)
58+
=>
59+
_out = out
60+
_ws = ws.WebSocketServer(auth, fd, this, config)
61+
62+
fun ref _websocket(): ws.WebSocketServer => _ws
63+
64+
fun ref on_upgrade_request(request: ws.UpgradeRequest val): Bool =>
65+
let origin = match request.header("Origin")
66+
| let o: String val => o
67+
| None => ""
68+
end
69+
70+
if (request.uri == "/ws") and (origin == "http://localhost") then
71+
_out.print("Accepted: uri=" + request.uri
72+
+ " origin=" + origin)
73+
true
74+
else
75+
_out.print("Rejected: uri=" + request.uri
76+
+ " origin=" + origin)
77+
false
78+
end
79+
80+
fun ref on_open(request: ws.UpgradeRequest val) =>
81+
_out.print("Client connected")
82+
83+
fun ref on_text_message(data: String val) =>
84+
_out.print("Echo: " + data)
85+
_ws.send_text(data)
86+
87+
fun ref on_closed(
88+
close_status: ws.CloseStatus,
89+
close_reason: String val)
90+
=>
91+
_out.print("Client disconnected: " + close_status.string())

0 commit comments

Comments
 (0)