Skip to content
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@
path = lib/multipart-parser-c
url = https://github.com/iafonov/multipart-parser-c.git
branch = main
[submodule "lib/usrsctp"]
path = lib/usrsctp
url = https://github.com/sctplab/usrsctp
92 changes: 92 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ LSQUIC_DIR = lib/lsquic
LSQUIC_BUILD_DIR = $(LSQUIC_DIR)/build
LSQUIC_LIB = $(LSQUIC_BUILD_DIR)/src/liblsquic/liblsquic.a

USRSCTP_DIR = lib/usrsctp
USRSCTP_BUILD_DIR = $(USRSCTP_DIR)/build
USRSCTP_LIB = $(USRSCTP_BUILD_DIR)/usrsctplib/libusrsctp.a

CURL_LIBS := $(shell pkg-config --libs libcurl 2>/dev/null)
NGHTTP2_LIBS := $(shell pkg-config --libs libnghttp2 2>/dev/null)
BROTLI_LIBS := $(shell pkg-config --libs libbrotlienc libbrotlicommon libbrotlidec 2>/dev/null)
Expand Down Expand Up @@ -140,6 +144,15 @@ ifeq ($(CWIST_WEBTRANSPORT),1)
CFLAGS += -DCWIST_WEBTRANSPORT
endif

# WebRTC DataChannel (src/net/webrtc/). DataChannel-only: SDP offer/answer,
# ICE-lite, DTLS via the vendored BoringSSL, and SCTP DataChannels (RFC 8831)
# through the lib/usrsctp submodule running in AF_CONN raw mode tunneled over
# DTLS. Set CWIST_WEBRTC=0 to leave it out.
CWIST_WEBRTC ?= 1
ifeq ($(CWIST_WEBRTC),1)
CFLAGS += -DCWIST_WEBRTC -I$(USRSCTP_DIR)/usrsctplib
endif

# Detect OS
IO_SRC = src/sys/io/io_select.c # Default fallback

Expand Down Expand Up @@ -699,6 +712,16 @@ EXTERNAL_LIBS = $(URIPARSER_LIB) \
$(BORINGSSL_SSL_LIB) \
$(BORINGSSL_CRYPTO_LIB)

ifeq ($(CWIST_WEBRTC),1)
SRCS += src/net/webrtc/webrtc.c \
src/net/webrtc/ice.c \
src/net/webrtc/sdp.c \
src/net/webrtc/dtls.c \
src/net/webrtc/sctp.c
EXTERNAL_LIBS += $(USRSCTP_LIB)
LIBS += $(USRSCTP_LIB)
endif

# --- Build Targets ---

all: $(LIBTTAK_LIB) $(CJSON_LIB) $(URIPARSER_LIB) $(SQLITE_DIR)/sqlite3.c $(LSQUIC_LIB) $(LIB_NAME)
Expand All @@ -716,6 +739,34 @@ $(SQLITE_DIR)/sqlite3.c:
# Ensure lsquic submodule is checked out before compiling objects that need its headers
$(OBJS): | lib/lsquic/include/lsquic.h

ifeq ($(CWIST_WEBRTC),1)
$(OBJS): | $(USRSCTP_DIR)/usrsctplib/usrsctp.h

$(USRSCTP_DIR)/usrsctplib/usrsctp.h:
@if [ ! -f "$@" ]; then \
echo "Initializing usrsctp submodule..."; \
git submodule update --init --recursive $(USRSCTP_DIR); \
fi

USRSCTP_REV := $(if $(wildcard $(USRSCTP_DIR)/.git),$(shell git -C $(USRSCTP_DIR) rev-parse HEAD 2>/dev/null))
USRSCTP_STAMP = $(USRSCTP_BUILD_DIR)/.usrsctp_built_$(or $(USRSCTP_REV),unversioned)

$(USRSCTP_LIB): $(USRSCTP_STAMP)

$(USRSCTP_STAMP):
@echo "Building usrsctp..."
@mkdir -p $(USRSCTP_BUILD_DIR)
cmake -S $(USRSCTP_DIR) -B $(USRSCTP_BUILD_DIR) \
-DCMAKE_C_COMPILER=$(CC) \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-Dsctp_build_programs=OFF \
-Dsctp_build_fuzzer=OFF
cmake --build $(USRSCTP_BUILD_DIR) --target usrsctp
@rm -f $(USRSCTP_BUILD_DIR)/.usrsctp_built_*
@touch $@
endif

lib/lsquic/include/lsquic.h:
@if [ ! -f "$@" ]; then \
echo "Initializing lsquic submodule..."; \
Expand Down Expand Up @@ -802,6 +853,7 @@ TEST_TARGETS = test_worker_affinity \
test_app_resource_limits \
test_classic_pool_scaling \
test_reactor_wake \
test_reactor_timer \
test_reactor_drain_chunk \
test_latency_probe \
test_sstring \
Expand Down Expand Up @@ -844,6 +896,7 @@ TEST_TARGETS = test_worker_affinity \
test_cors \
test_websocket \
test_websocket_async \
test_webrtc \
test_jwt \
test_migrate \
test_json_heal \
Expand Down Expand Up @@ -927,6 +980,7 @@ bench_security_pool: $(LIB_NAME) tests/bench_security_pool.c
$(CC) $(CFLAGS) -o bench_security_pool tests/bench_security_pool.c $(LIB_NAME) $(LIBS)
./bench_security_pool

# WebRTC DataChannel end-to-end test (see the test_webrtc rule below).
test: $(TEST_TARGETS)

src/sys/app/app.o: src/sys/app/worker_affinity.h
Expand All @@ -952,6 +1006,14 @@ test_reactor_wake: tests/test_reactor_wake.c src/sys/io/reactor.c
$(CC) $(CFLAGS) -o $@ tests/test_reactor_wake.c -pthread
./$@

# Reactor one-shot timers, on the default backend and on forced epoll.
test_reactor_timer: tests/test_reactor_timer.c src/sys/io/reactor.c
$(CC) $(CFLAGS) -o $@ tests/test_reactor_timer.c -pthread
./$@
ifeq ($(UNAME_S),Linux)
CWIST_REACTOR_BACKEND=epoll ./$@
endif

# Cooperative-queuing correctness test (issue #25): CWIST_REACTOR_DRAIN_CHUNK
# interleaves foreign-thread post draining into a big CQE batch instead of
# only at the batch's end. See tests/bench_cooperative_queuing.c for the
Expand Down Expand Up @@ -1463,6 +1525,36 @@ test_multipart: $(LIB_NAME) tests/test_multipart.c
$(CC) $(CFLAGS) -o test_multipart tests/test_multipart.c $(LIB_NAME) $(LIBS)
./test_multipart

ifeq ($(CWIST_WEBRTC),1)
test_webrtc: $(LIB_NAME) $(USRSCTP_LIB) tests/test_webrtc.c
$(CC) $(CFLAGS) -o test_webrtc tests/test_webrtc.c $(LIB_NAME) $(LIBS)
./test_webrtc

# Loopback DataChannel benchmark: idle cost, throughput, RTT. Not part of
# `make test`; run ./bench_webrtc [small_count] [large_count] after building.
bench_webrtc: $(LIB_NAME) $(USRSCTP_LIB) tests/bench_webrtc.c
$(CC) $(CFLAGS) -o bench_webrtc tests/bench_webrtc.c $(LIB_NAME) $(LIBS)

# Real-browser interop: starts example/webrtc and drives headless Chromium
# against it (tests/browser/webrtc_chromium.mjs). Needs chromium and Node >= 22;
# not part of `make test`. Uses port 8080.
test_webrtc_browser: $(LIB_NAME) $(USRSCTP_LIB)
$(MAKE) -C example/webrtc
@(cd example/webrtc && exec env CWIST_WORKERS=1 ./webrtc-server > /dev/null 2>&1) & pid=$$!; \
sleep 1; \
node tests/browser/webrtc_chromium.mjs http://localhost:8080/; rc=$$?; \
kill $$pid 2>/dev/null; wait $$pid 2>/dev/null; \
exit $$rc

.PHONY: test_webrtc bench_webrtc test_webrtc_browser
else
# CWIST_WEBRTC=0: the module and its test are compiled out.
test_webrtc bench_webrtc test_webrtc_browser:
@echo "CWIST_WEBRTC=0: skipping $@"

.PHONY: test_webrtc bench_webrtc test_webrtc_browser
endif

test_waf: $(LIB_NAME) tests/test_waf.c
$(CC) $(CFLAGS) -o test_waf tests/test_waf.c $(LIB_NAME) $(LIBS)
./test_waf
Expand Down
25 changes: 25 additions & 0 deletions example/webrtc/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
CC ?= gcc
CFLAGS = -Wall -Wextra -g -I../../include

ROOT = ../..
LIBS = $(ROOT)/libcwist.a \
$(ROOT)/lib/cnats/build/lib/libnats_static.a \
$(ROOT)/lib/libttak/lib/libttak.a \
$(ROOT)/lib/cjson/libcjson.a \
$(ROOT)/lib/uriparser/build/liburiparser.a \
$(ROOT)/lib/lsquic/build/src/liblsquic/liblsquic.a \
$(ROOT)/lib/usrsctp/build/usrsctplib/libusrsctp.a \
$(ROOT)/lib/boringssl/build/libssl.a \
$(ROOT)/lib/boringssl/build/libcrypto.a \
-pthread -ldl -lm -lstdc++ -lz -lcurl -lnghttp2

SRCS = main.c
TARGET = webrtc-server

all: $(TARGET)

$(TARGET): $(SRCS) $(ROOT)/libcwist.a
$(CC) $(CFLAGS) -o $(TARGET) $(SRCS) $(LIBS)

clean:
rm -f $(TARGET)
45 changes: 45 additions & 0 deletions example/webrtc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# CWIST WebRTC DataChannel example

DataChannel-only WebRTC echo server: the browser sends an SDP offer over
HTTP POST, the CWIST server (acting as an ICE-lite endpoint) answers, and
both ends run ICE + DTLS + SCTP (RFC 8831) so messages on a DataChannel are
echoed back.

## Build

```sh
make -C ../.. libcwist.a # builds lib/usrsctp too (CWIST_WEBRTC=1 default)
make
```

## Run

```sh
./webrtc-server
# signaling: http://localhost:8080/
# [webrtc] ctx ready in pid <worker> on UDP port <ephemeral> (first offer)
```

Open http://localhost:8080/ in a browser, click **Connect**, type a message
and press **Send**. Every DataChannel message is echoed back and printed on
the server console.

## Layout

- `main.c` — cwist app serving `index.html` (GET /) and SDP answers
(POST /offer, `application/sdp`), plus the echo logic on the cwist webrtc
ctx. `cwist_app_listen` forks one HTTP worker per core and a ctx belongs to
the process that created it, so each worker creates its own ctx (own UDP
port, own reactor thread) on its first offer; the answer carries that
port. Message, channel and close handlers run on the ctx's reactor thread.
- `index.html` — minimal RTCPeerConnection client using a DataChannel.

## Notes / limitations (MVP)

- The server is ICE-lite: it answers STUN binding requests and treats a
valid `USE-CANDIDATE` request as the nominated pair. The browser must be
the controlling agent (the default).
- The browser connects to the host candidate advertised in the answer
(`a=candidate:` with the server's best-guess local IPv4). Set the env
var or edit the code behind a NAT; there is no STUN/TURN server support.
- Ordered delivery only; the answer pins `max-message-size:262144`.
48 changes: 48 additions & 0 deletions example/webrtc/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CWIST WebRTC DataChannel echo</title>
</head>
<body>
<h1>CWIST WebRTC DataChannel echo</h1>
<button id="connect">Connect</button>
<input id="msg" placeholder="type a message" size="40">
<button id="send">Send</button>
<pre id="log"></pre>
<script>
const log = (s) => { document.getElementById('log').textContent += s + "\n"; };
let pc = null, dc = null;

document.getElementById('connect').onclick = async () => {
pc = new RTCPeerConnection({ iceServers: [] });
dc = pc.createDataChannel('echo');
dc.onopen = () => log('datachannel open');
dc.onmessage = (ev) => log('<< ' + (typeof ev.data === 'string' ? ev.data : '<binary>'));

pc.onicecandidate = async (ev) => {
if (ev.candidate) return; // ICE-lite answer: wait for gathering to finish
const res = await fetch('/offer', {
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
body: pc.localDescription.sdp,
});
const answer = await res.text();
await pc.setRemoteDescription({ type: 'answer', sdp: answer });
log('answer set');
};

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
};

document.getElementById('send').onclick = () => {
const v = document.getElementById('msg').value;
if (dc && dc.readyState === 'open') {
dc.send(v);
log('>> ' + v);
}
};
</script>
</body>
</html>
109 changes: 109 additions & 0 deletions example/webrtc/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* @file main.c
* @brief WebRTC DataChannel echo server: SDP signaling over HTTP POST,
* ICE-lite + DTLS + SCTP DataChannel echo over UDP.
*
* Open http://localhost:8080/ in a browser, click Connect, and type
* messages: the server echoes every DataChannel message back.
*/
#include <cwist/app.h>
#include <cwist/net/webrtc.h>

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* One webrtc ctx per process. cwist_app_listen() forks HTTP workers, and a
* ctx (with its reactor thread) belongs to the process that created it, so
* each worker makes its own on its first offer. Every answer names the UDP
* port of the ctx that wrote it, so browsers reach the right process. */
static cwist_webrtc_ctx *g_webrtc;
static pthread_once_t g_webrtc_once = PTHREAD_ONCE_INIT;

static void on_message(cwist_webrtc_conn *conn, uint16_t channel, const uint8_t *data, size_t len,
cwist_webrtc_data_type type, void *user) {
(void)user;
(void)channel;
if (type == CWIST_WEBRTC_DATA_STRING)
printf("[webrtc] text (%zu bytes): %.*s\n", len, (int)len, (const char *)data);
else
printf("[webrtc] binary (%zu bytes)\n", len);
cwist_webrtc_conn_send(conn, channel, data, len, type); /* echo with the same type */
}

static void on_channel(cwist_webrtc_conn *conn, uint16_t channel, const char *label,
void *user) {
(void)conn;
(void)user;
printf("[webrtc] channel %u opened by peer (label=%s)\n", channel, label);
}

static void on_close(cwist_webrtc_conn *conn, void *user) {
(void)conn;
(void)user;
printf("[webrtc] connection closed\n");
}

static void index_handler(cwist_http_request *req, cwist_http_response *res) {
(void)req;
FILE *f = fopen("index.html", "rb");
if (!f) {
/* Fall back to the source directory layout. */
f = fopen("example/webrtc/index.html", "rb");
}
if (!f) {
cwist_http_header_add(&res->headers, "Content-Type", "text/plain");
cwist_sstring_assign(res->body, "index.html not found");
return;
}
char html[16384];
size_t n = fread(html, 1, sizeof(html) - 1, f);
fclose(f);
html[n] = '\0';
cwist_http_header_add(&res->headers, "Content-Type", "text/html");
cwist_sstring_assign(res->body, html);
}

static void webrtc_init(void) {
g_webrtc = cwist_webrtc_ctx_new(0);
if (!g_webrtc) {
fprintf(stderr, "failed to create webrtc ctx\n");
return;
}
cwist_webrtc_ctx_set_message_handler(g_webrtc, &on_message, NULL);
cwist_webrtc_ctx_set_channel_handler(g_webrtc, &on_channel, NULL);
cwist_webrtc_ctx_set_close_handler(g_webrtc, &on_close, NULL);
printf("[webrtc] ctx ready in pid %d on UDP port %u\n", (int)getpid(),
cwist_webrtc_ctx_port(g_webrtc));
}

static void offer_handler(cwist_http_request *req, cwist_http_response *res) {
pthread_once(&g_webrtc_once, webrtc_init);
/* Handlers run on several HTTP threads at once: no static buffer. */
char answer[4096];
if (!g_webrtc ||
cwist_webrtc_handle_offer(g_webrtc, req->body->data, answer, sizeof(answer)) < 0) {
cwist_http_header_add(&res->headers, "Content-Type", "text/plain");
cwist_sstring_assign(res->body, "invalid SDP offer");
res->status_code = CWIST_HTTP_BAD_REQUEST;
return;
}
printf("[webrtc] answered offer; UDP port %u, fingerprint %s\n",
cwist_webrtc_ctx_port(g_webrtc), cwist_webrtc_ctx_fingerprint(g_webrtc));
cwist_http_header_add(&res->headers, "Content-Type", "application/sdp");
cwist_sstring_assign(res->body, answer);
}

int main(void) {
cwist_app *app = cwist_app_create();
cwist_app_get(app, "/", index_handler);
cwist_app_post(app, "/offer", offer_handler);
printf("signaling: http://localhost:8080/\n");
cwist_app_listen(app, 8080);
/* Every worker returns here; each frees the ctx it created, if any. */
cwist_app_destroy(app);
cwist_webrtc_ctx_free(g_webrtc);
return 0;
}
Loading
Loading