HTTP(S) + WS(S) + Terminal MUX
A cross-platform C library for building network daemons - HTTP servers, WebSocket servers, telnet-like services, terminal multiplexers, or custom network applications.
From NeverDark • Powers tty.pt
libndc - C library for building network daemons
ndc - Standalone server binary with HTTP/WS/terminal mux
Build telnet-like servers, custom protocol handlers, HTTP APIs, WebSocket apps, or anything that needs persistent network connections with an event loop.
| Platform | Status |
|---|---|
| Linux, macOS, BSD | ✅ Full support |
| Windows |
# Run simple HTTP server
ndc -d -p 8888
# With SSL (POSIX)
sudo ndc -C . -K certs.txt -d| Option | Description |
|---|---|
-p PORT |
Specify HTTP server port |
-s PORT |
Specify HTTPS server port (POSIX) |
-C PATH |
Change directory to PATH before starting |
-K PATH |
Load SSL certificate mappings from file (POSIX) |
-k CERT |
Add single SSL certificate mapping (POSIX) |
-d |
Don't detach (run in foreground) |
-r |
Root multiplex mode |
-? |
Display help message |
example.com:cert.pem:key.pem#include <ttypt/ndc.h>
int main(void) {
ndc_config.port = 8080;
ndc_register("GET", do_GET, CF_NOAUTH);
return ndc_main(); // Blocks, runs event loop
}void cmd_echo(socket_t fd, int argc, char *argv[]) {
for (int i = 0; i < argc; i++)
ndc_writef(fd, "%s ", argv[i]);
ndc_writef(fd, "\n");
}
int ndc_connect(socket_t fd) {
ndc_writef(fd, "Welcome! Type 'echo hello'\n");
return 1; // Accept connection
}
int main(void) {
ndc_config.port = 2323;
ndc_register("echo", cmd_echo, CF_NOAUTH);
return ndc_main();
}void my_handler(socket_t fd, char *body) {
// Handle raw data from client
ndc_write(fd, "RESPONSE", 8);
}
void ndc_command(socket_t fd, int argc, char *argv[]) {
// Called on every command
log_command(argv[0]);
}
void ndc_update(unsigned long long dt) {
// Periodic tick (game loops, etc)
}| Function | Description | Return Value |
|---|---|---|
ndc_main() |
Start event loop (blocking) | Exit code |
ndc_register(name, cb, flags) |
Register command handler | - |
ndc_register_handler(path, handler) |
Register HTTP handler for exact path | - |
ndc_write(fd, data, len) |
Write raw bytes | Bytes written or -1 |
ndc_writef(fd, fmt, ...) |
Write formatted data | Bytes written or -1 |
ndc_dwritef(fd, fmt, va) |
Write formatted data with va_list | Bytes written or -1 |
ndc_close(fd) |
Close connection | - |
ndc_wall(msg) |
Broadcast message to all descriptors | - |
| Function | Description | Return Value |
|---|---|---|
ndc_header(fd, key, val) |
Add response header (call before ndc_head) | - |
ndc_head(fd, code) |
Send HTTP status and headers | - |
ndc_body(fd, body) |
Send body and close connection | - |
ndc_sendfile(fd, path) |
Serve static file with auto MIME type | - |
ndc_status_text(code) |
Get HTTP status text for code | Status string |
| Function | Description | Return Value |
|---|---|---|
ndc_flags(fd) |
Get descriptor flags | Flags bitmask |
ndc_set_flags(fd, flags) |
Set descriptor flags | - |
| Function | Description | Return Value |
|---|---|---|
ndc_env_get(fd, target, key) |
Get request environment value | 0 on success |
ndc_env_put(fd, key, value) |
Set environment key/value | 0 on success |
ndc_env_clear(fd) |
Clear request environment | - |
ndc_env(fd) |
Get internal env handle (advanced) | Env handle |
| Function | Description | Return Value |
|---|---|---|
ndc_pty(fd, args[]) |
Spawn PTY-backed command | - |
ndc_exec(fd, args[], cb, input, len) |
Execute command with callback | - |
ndc_auth(fd, username) |
Mark user as authenticated, drop privileges (POSIX) | 0 on success, 1 on failure |
ndc_cert_add(str) |
Add cert mapping: domain:cert.pem:key.pem |
- |
ndc_certs_add(fname) |
Load certificate mappings from file | - |
ndc_mmap(mapped, file) |
Map file into memory | File size or -1 |
ndc_mmap_iter(start, pos) |
Iterate mapped lines separated by \n |
Next line or NULL |
ndc_sendfile(fd, path) |
Serve static file (POSIX uses sendfile syscall) | - |
| Handler | Description |
|---|---|
do_GET |
HTTP GET request handler |
do_POST |
HTTP POST request handler |
do_sh |
Shell PTY handler (POSIX-only) |
Define these weak symbol hooks to customize behavior:
| Hook | Description | Return Value |
|---|---|---|
ndc_connect(socket_t fd) |
Accept/reject WebSocket connections | Non-zero to accept |
ndc_disconnect(socket_t fd) |
Cleanup on disconnect | - |
ndc_accept(socket_t fd) |
Called on socket accept | Ignored |
ndc_command(socket_t fd, int argc, char *argv[]) |
Before command execution | - |
ndc_flush(socket_t fd, int argc, char *argv[]) |
After command execution | - |
ndc_vim(socket_t fd, int argc, char *argv[]) |
Called when command not found | - |
ndc_update(unsigned long long dt) |
Periodic updates (dt in milliseconds) | - |
ndc_auth_check(socket_t fd) |
Custom auth hook: validate credentials, return username. Default: session file lookup in ./sessions/ |
Username string or NULL |
Example:
int ndc_connect(socket_t fd) {
ndc_writef(fd, "Welcome!\n");
return 1; // Accept connection
}
void ndc_disconnect(socket_t fd) {
// Cleanup resources
}
void ndc_command(socket_t fd, int argc, char *argv[]) {
// Log or validate commands
}
void ndc_flush(socket_t fd, int argc, char *argv[]) {
// Post-command processing
}
void ndc_vim(socket_t fd, int argc, char *argv[]) {
ndc_writef(fd, "Unknown command: %s\n", argv[0]);
}
void ndc_update(unsigned long long dt) {
// Game loop, periodic tasks, etc.
}
char *ndc_auth_check(socket_t fd) {
// Check cookies, tokens, etc.
// Return username or NULL
return authenticated_user;
}struct ndc_config {
char *chroot; // chroot directory (POSIX)
unsigned flags; // Server flags (see below)
unsigned port; // HTTP listen port
unsigned ssl_port; // HTTPS listen port (POSIX)
ndc_handler_t *default_handler; // Fallback HTTP handler
};
// Example usage
ndc_config.port = 8080; // HTTP on port 8080
ndc_config.ssl_port = 8443; // HTTPS on port 8443 (POSIX)
ndc_config.flags = NDC_SSL; // Enable TLS
ndc_config.chroot = "/var/www"; // chroot directory (POSIX)
ndc_config.default_handler = my_404; // Custom 404 handler| Flag | Description |
|---|---|
NDC_WAKE |
Wake on activity |
NDC_SSL |
Enable TLS/SSL |
NDC_ROOT |
Root multiplex mode |
NDC_SSL_ONLY |
Redirect HTTP to HTTPS when SSL enabled |
NDC_DETACH |
Detach into background (daemon mode) |
Use with ndc_register():
| Flag | Description |
|---|---|
CF_NOAUTH |
Allow command without authentication |
CF_NOTRIM |
Do not trim trailing CR from input |
Access with ndc_flags() and ndc_set_flags():
| Flag | Description |
|---|---|
DF_CONNECTED |
Connection established and accepted |
DF_WEBSOCKET |
WebSocket mode enabled |
DF_TO_CLOSE |
Marked to close after remaining output |
DF_ACCEPTED |
Accepted socket (pre-WebSocket) |
DF_AUTHENTICATED |
User authenticated |
| Feature | POSIX | Windows |
|---|---|---|
| HTTP/WebSocket | ✅ | ✅ |
| Custom commands | ✅ | ✅ |
| PTY/Terminal | ✅ | ❌ |
| CGI execution | ✅ | ❌ |
| Authentication (privilege dropping) | ✅ | ❌ |
| SSL certs | ✅ | ❌ |
Windows build provides core networking only.
Create index.sh for dynamic pages:
#!/bin/sh
# CGI scripts output status line without "HTTP/1.1" prefix
printf "200 OK\r\n"
printf "Content-Type: text/plain\r\n"
printf "\r\n"
printf "Hello world\n"
printf "REQUEST_METHOD=%s\n" "$REQUEST_METHOD"
printf "QUERY_STRING=%s\n" "$QUERY_STRING"Control access with serve.allow and serve.autoindex files.
Install the package:
npm install @tty-pt/ndcJavaScript/TypeScript API:
import { create } from "@tty-pt/ndc";
// Create terminal instance
const term = create(document.getElementById("terminal"), {
proto: "ws", // or "wss" for secure
port: 4201,
sub: {
onOpen: (term, ws) => {
console.log("Connected to server");
},
onClose: (ws) => {
console.log("Disconnected, reconnecting...");
},
onMessage: (ev, arr) => {
// Return true to continue default processing
return true;
},
cols: 80,
rows: 25,
},
debug: false,
});See types/ndc.d.ts for full TypeScript definitions.
- Man pages:
man ndcandman ndc.3 - Full API:
include/ttypt/ndc.h - Examples:
src/test-*.c
Load dynamic modules with dependency resolution via libndx:
// In your plugin module
const char *ndx_deps[] = { "dependency.so", NULL };
// In main application
ndx_load("plugin.so"); // Automatically loads dependenciesThe binary automatically loads core.so at startup. Plugins can hook into lifecycle events (ndc_update, ndc_connect, etc.) to extend functionality.
Installation: See install docs
Entry points: src/ndc.c (native), ndc-cli.js (npm)
