Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
005e946
add WebSocket endpoint support
heshammahamed Jun 28, 2026
6fa0377
implement WebSocket handler construction
heshammahamed Jun 28, 2026
1d9be51
Fix WebSocket handler generation in server.alusus
heshammahamed Jun 30, 2026
37b4ea0
Generate URI checks for @wsEndpoint paths in requestCallback
heshammahamed Jun 30, 2026
6ea9e7d
Replace shared connection Map with per-connection userData
heshammahamed Jul 5, 2026
66655c3
refactor(websocket): declare endpoint handlers per-function instead o…
heshammahamed Jul 6, 2026
df16361
map string to ptr -- map[String , ptr[TiObject]] -- instead of mappin…
heshammahamed Jul 7, 2026
39a8fee
correct sume bugs in function extractElementsWithGivenModifier inside…
heshammahamed Jul 7, 2026
ac575d0
feat(websocket): make onConnect/onReady/onData/onClose handlers optional
heshammahamed Jul 9, 2026
3fb6208
fix(websocket): remove trailing comma in setWebSocketHandler ast temp…
heshammahamed Jul 10, 2026
9d2d81b
feat(websocket): add handshake info snapshot to wsConnection
heshammahamed Jul 10, 2026
5d079af
feat(websocket): add maxMessageSize, fragmentation reassembly, and cl…
heshammahamed Jul 11, 2026
ca99d38
(websoxket) Capitalize class name from ws_connection to Ws_connection
heshammahamed Jul 11, 2026
76c7679
(websocket) : return back to classes
heshammahamed Jul 14, 2026
ee79a82
(websocket): instantiate user's WsRoute-derived class per endpoint
heshammahamed Jul 15, 2026
39a960a
(server): enable CivetWeb's built-in ping/pong handling
heshammahamed Jul 15, 2026
68d4b32
refactor(server): remove duplicate function, rename extractModifiersP…
heshammahamed Jul 15, 2026
e87eff2
feat(websocket): store close status code and reason for exposure via …
heshammahamed Jul 19, 2026
dc49adf
fix(websocket): segfault in close() methode when a reason message is…
heshammahamed Jul 19, 2026
6edc04b
feat(websocket): handle write failures in send/close, add request/web…
heshammahamed Jul 19, 2026
d9b2c14
feat(websocket): add getStatus() and CLOSING transition to close paths
heshammahamed Jul 19, 2026
ae4d794
fix(websocket): reject unrecognized/reserved opcodes as a protocol error
heshammahamed Jul 19, 2026
c239f2a
Clean up the design of web sockets
sarmadka Jul 30, 2026
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
2 changes: 2 additions & 0 deletions WebPlatform.alusus
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ import "Spp/Ast";
import "Build";
import "Apm";
import "closure";
Apm.importPackage("Alusus/Sle@0.2", "Srl/enums.alusus");
Apm.importPackage("Alusus/Http@0.3");
Apm.importPackage("Alusus/Json@0.2");
Apm.importPackage("Alusus/MarkdownTranslator@0.1");
Apm.importPackage("Alusus/Promises@0.1");

import "WebPlatform/server";
import "WebPlatform/WsConnection";
import "WebPlatform/browser_api";
import "WebPlatform/frontend_helpers";
import "WebPlatform/Styling/Color";
Expand Down
212 changes: 212 additions & 0 deletions WebPlatform/WsConnection.alusus
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
@merge module WebPlatform {
class WsStatus {
setupStringEnum[];
enumStringValue[CONNECTING, "connecting"];
enumStringValue[OPENED, "opened"];
enumStringValue[CLOSING, "closing"];
enumStringValue[CLOSED, "closed"];
}

class WsConnection {
def wkThis: WkRef[this_type];
def connection: ptr[Http.Connection];

def status: WsStatus;
def closeCode: word[16] = 1006;
def closeReason: String;

def maxMessageSize: ArchInt = 1024;
def fragmentBuffer: StringBuilder();
def fragmentOpcode: Int;
def isFragmenting: Bool = 0;

handler this~init(conn: ptr[Http.Connection]) {
this.connection = conn;
this.status = WsStatus.CONNECTING;
}

handler this.getStatus(): WsStatus {
return this.status;
}

handler this.setMaxMessageSize(size: ArchInt) {
this.maxMessageSize = size;
fragmentBuffer.bufferGrowSize = (maxMessageSize / 2)~cast[ArchInt];
}

handler this.getMaxMessageSize(): ArchInt{
return this.maxMessageSize;
}

handler this.sendText(data: CharsPtr) {
if this.status != WsStatus.OPENED return ;

if Http.writeTextToWebSocket(this.connection, data) <= 0 {
this.status = WsStatus.CLOSED;
}
}

handler this.sendBinary(data: CharsPtr, dataLen: ArchWord) {
if this.status != WsStatus.OPENED return ;
if Http.writeBinaryToWebSocket(this.connection , data , dataLen) <= 0 {
this.status = WsStatus.CLOSED;
}
}

handler this.close () {
this.close(1000 , "")
}

handler this.close (statusCode : word[16] , reasonMessage : CharsPtr) {
if this.status != WsStatus.OPENED return;

this.closeCode = statusCode;
this.closeReason = reasonMessage;

def reasonLen : Int = String.getLength(reasonMessage);

def payloadLen : Int = 2 + reasonLen;

if (payloadLen > 125) {
reasonLen = 123; // 125 - 2 bytes for the status code
payloadLen = 125;
}

// close frame payload max is 125 bytes as stated in RFC 6455 §5.5.1
def payload : array[Char, 125];

payload(0) = (statusCode >> 8)[ptr[Char]] & 0xFF; // status code, high byte
payload(1) = statusCode & 0xFF; // status code, low byte

if (reasonLen > 0) {
Memory.copy(payload~ptr~cast[ptr[Char]] + 2, reasonMessage, reasonLen~cast[ArchInt]);
}

this.status = WsStatus.CLOSING;

if Http.writeToWebSocket(this.connection, 8, payload~ptr, payloadLen) <= {
this.status = WsStatus.CLOSED;
}

return result;
}

// Called internally by the library to respond to a client-initiated
// close frame, completing the close handshake as stated in
// RFC 6455 §5.5.1 (a responder typically echoes the status code
// it received).
handler this.replyToClose (data : CharsPtr, dataLen : ArchWord) {
if this.status != WsStatus.OPENED return;

// Extract and store the close code/reason before replying,
// per RFC 6455 §5.5.1: first 2 bytes = status code, rest = reason.

if (dataLen~cast[Int] >= 2) {
this.closeCode = ((data~cnt(0)~cast[word[16]]) << 8) | data~cnt(1)~cast[word[16]];
if (dataLen~cast[Int] > 2) {
def reasonLen: Int = (dataLen~cast[Int] - 2);
this.closeReason = String(data + 2, reasonLen);
}
} else {
// client sent a close frame with no code at all — valid per spec
this.closeCode = 1005; // "No Status Received"
this.closeReason = "";
}

this.status = WsStatus.CLOSING;

if Http.writeToWebSocket(this.connection, 8, data, dataLen) <= 0 {
this.status = WsStatus.CLOSED;
}

return result;
}

handler this.onConnect(): Int as_ptr {
return 0;
}

handler this.onReady() as_ptr {
}

handler this.onData(bits: Int, data: CharsPtr, dataLen: ArchWord): Int {
def opcode : Int = bits & 0x0F;
def fin: Bool = (bits & 0x80) != 0;

// Safe to cast: CivetWeb rejects any frame exceeding ~2 GiB (0x7FFF0000),
// which is within ArchInt's range, so this cast can't overflow or go negative.

def dataLen : ArchInt = dataLen~cast[ArchInt];

if opcode == 1 or opcode == 2 {
if (this.isFragmenting) {
this.close(1002, "unexpected new message mid-fragment");
return 0;
}

this.fragmentOpcode = opcode;
this.fragmentBuffer.clear();

if (dataLen > this.getMaxMessageSize()) {
this.close(1009, "Message too big");
return 0;
}

this.fragmentBuffer.append(data, dataLen);

if (fin) {
def isBinary : Bool = this.fragmentOpcode == 2;
this.onUserData(this.fragmentBuffer.string, isBinary);
this.fragmentBuffer.clear();
} else {
this.isFragmenting = 1;
}
}

if opcode == 0 {
if (!this.isFragmenting) {
// Protocol violation: CONTINUATION with no preceding TEXT/BINARY
this.close(1002, "unexpected continuation frame");
return 0;
}

if (this.fragmentBuffer.getLength() + dataLen > this.getMaxMessageSize()) {
this.close(1009, "Message too big");
return 0;
}

this.fragmentBuffer.append(data, dataLen);

if (fin) {
def isBinary : Bool = this.fragmentOpcode == 2;
this.onUserData(this.fragmentBuffer.string, isBinary);
this.fragmentBuffer.clear();
this.isFragmenting = 0;
}
}

if opcode == 8 {
// reply with a close frame before tearing down — completes the handshake properly
if dataLen~cast[Int] >= 2 {
this.replyToClose(data, dataLen); // echo back client's code+reason
} else {
this.close(1000, ""); // client sent no code, reply with normal closure
}
return 0;
}

if (opcode != 0 and opcode != 1 and opcode != 2 and opcode != 8) {
this.close(1002, "unsupported opcode");
return 0;
}

return 1;
}

handler this.onUserData(data: ref[String], isBinary: Bool) as_ptr {
}

handler this.onClose() as_ptr {
}
}
}
Loading