Skip to content

ougi-washi/Syphax-Web

Repository files navigation

Syphax-Web

Small C web toolkit for server-rendered apps.

  • HTTP/1.1 server
  • optional OpenSSL TLS
  • HTML builder
  • small JS helpers
  • JSON-backed translations
  • optional SQLite/PostgreSQL SQL helper
  • CMake install package

Build

./build.sh

TLS build:

./build.sh -tls

Encrypted login tokens:

./build.sh -crypto

SQLite database support:

./build.sh -sqlite

PostgreSQL database support:

./build.sh -postgres

SQLite and PostgreSQL:

./build.sh -db

Install:

cmake --install build --prefix /your/prefix

Stress tool:

bin/server_stress --mode mixed --workers 4 --connections 1000 --requests 10000

Link

find_package(syphax_web CONFIG REQUIRED)
target_link_libraries(my_target PRIVATE syphax_web::syphax_web)

Minimal Server

#include "sw_html.h"
#include "sw_server.h"

static void handler(sw_connection* c, const sw_http_message* hm, void* user_data) {
    (void)user_data;

    if (!sw_http_is(hm, "GET", "/")) {
        sw_http_replyf(c, 404, "text/plain; charset=utf-8", "Not Found");
        return;
    }

    sw_buffer* h = sw_buffer_new();

    sw_html(h, sw_attrs(sw_attr("lang", "en")), {
        sw_body(h, sw_attrs(), {
            sw_h1(h, sw_attrs(), {
                sw_text(h, "Syphax Web");
            });
        });
    });

    sw_http_reply(c, 200, "text/html; charset=utf-8", sw_buffer_data(h), sw_buffer_len(h));
    sw_buffer_free(h);
}

int main(void) {
    return sw_server_listen("http://0.0.0.0:8000", NULL, handler, NULL);
}

Open http://127.0.0.1:8000.

Pass NULL for defaults. Use sw_server_config_default() to override backlog, worker count, connection limits, buffer caps, keep-alive limits, and timeouts.

Long-running servers can be managed explicitly:

sw_server_config config = sw_server_config_default();
config.worker_count = 4;

sw_server* server = sw_server_create(&config);
sw_server_add_http(server, "http://0.0.0.0:8000", handler, NULL);
sw_server_run(server);
sw_server_destroy(server);

Cookies And Sessions

char theme[32];
sw_sessions* sessions = sw_sessions_create(NULL);

sw_http_get_cookie(hm, "theme", theme, sizeof(theme));
sw_http_set_cookie(c, "theme", "dark", NULL);

sw_session* session = sw_sessions_start(sessions, c, hm);
sw_session_set(session, "user_id", "42");

Default cookies use Path=/, HttpOnly, SameSite=Lax, and Secure on TLS connections.

Encrypted Login Tokens

Build with ./build.sh -crypto or ./build.sh -tls.

sw_tokens* tokens = sw_tokens_create(NULL);

sw_token* token = sw_tokens_login(tokens, c, hm, user_id);
sw_token_set(token, "role", "admin");

token = sw_tokens_current(tokens, c, hm);
const c8* user_id = sw_token_get(token, "user_id");

sw_tokens_logout(tokens, c, hm);

Apps still check passwords and load users. Syphax-Web stores token data in memory and sends only an encrypted cookie.

Database

Build with ./build.sh -sqlite, ./build.sh -postgres, or ./build.sh -db.

#include "sw_db.h"

sw_db* db = sw_db_open(NULL); /* sqlite://:memory: when SQLite is enabled */

sw_db_exec(db, "CREATE TABLE notes (title TEXT, hits INTEGER)");

sw_db_stmt* stmt = sw_db_prepare(db, "INSERT INTO notes(title, hits) VALUES(?, ?)");
sw_db_bind_text(stmt, 1, "Hello");
sw_db_bind_int(stmt, 2, 1);
sw_db_step(stmt);
sw_db_finalize(stmt);

stmt = sw_db_prepare(db, "SELECT title, hits FROM notes WHERE hits = ?");
sw_db_bind_int(stmt, 1, 1);
while (sw_db_step(stmt) == SW_DB_ROW) {
    const c8* title = sw_db_column_text(stmt, 0);
    i64 hits = sw_db_column_int(stmt, 1);
    (void)title;
    (void)hits;
}
sw_db_finalize(stmt);
sw_db_close(db);

Use sqlite://path.db, sqlite://:memory:, postgres://..., or postgresql://.... bin/07_database uses SQLite by default; set SYPHAX_WEB_DB_URL for PostgreSQL-only runs.

TLS

Native HTTPS is enabled with ./build.sh -tls.

int main(void) {
    sw_tls_config tls = sw_tls_config_default();

    tls.certificate_file = "/etc/ssl/example/fullchain.pem";
    tls.private_key_file = "/etc/ssl/example/privkey.pem";

    return sw_server_listen_tls("https://0.0.0.0:8443", NULL, &tls, handler, NULL);
}

Local example certs:

TRUST_STORES=system,nss mkcert -install
mkcert \
  -cert-file examples/shared/localhost.local.crt \
  -key-file examples/shared/localhost.local.key \
  localhost 127.0.0.1 ::1

./build.sh -tls
./bin/02_https

The TLS examples also accept:

SYPHAX_WEB_TLS_CERT=/path/to/fullchain.pem \
SYPHAX_WEB_TLS_KEY=/path/to/privkey.pem \
./bin/02_https

Static Files

Serve public assets from a fixed docroot:

if (sw_http_is(hm, "GET", "/style.css")) {
    sw_http_serve_path(c, "resources", hm->uri);
    return;
}

sw_http_serve_path keeps requests inside the docroot.

Native Modals

Modal behavior uses the browser's native <dialog> element. Syphax-Web only binds events to dialog actions; markup, content, validation, and styling stay in the application.

#include "sw_js.h"

sw_button(h, sw_attrs(
    sw_attr_no_translate("id", "open-settings"),
    sw_attr_no_translate("type", "button")
), {
    sw_text(h, "Open Settings");
});

sw_dialog(h, sw_attrs(
    sw_attr_no_translate("id", "settings-dialog"),
    sw_attr_no_translate("aria-labelledby", "settings-title")
), {
    sw_h2(h, sw_attrs(sw_attr_no_translate("id", "settings-title")), {
        sw_text(h, "Settings");
    });
    sw_p(h, sw_attrs(), {
        sw_text(h, "Application-owned modal content.");
    });
    sw_div(h, sw_attrs(sw_attr_no_translate("class", "actions")), {
        sw_form(h, sw_attrs(sw_attr_no_translate("method", "dialog")), {
            sw_button(h, sw_attrs(
                sw_attr_bool("autofocus", 1),
                sw_attr_no_translate("type", "submit"),
                sw_attr_no_translate("value", "done")
            ), {
                sw_text(h, "Done");
            });
        });
        sw_button(h, sw_attrs(
            sw_attr_no_translate("id", "request-settings-close"),
            sw_attr_no_translate("type", "button")
        ), {
            sw_text(h, "Cancel");
        });
        sw_button(h, sw_attrs(
            sw_attr_no_translate("id", "force-settings-close"),
            sw_attr_no_translate("type", "button")
        ), {
            sw_text(h, "Close Now");
        });
    });
});

sw_js_modal(h,
    .trigger_id = "open-settings",
    .target_id = "settings-dialog",
    .close_on_backdrop = 1
);
sw_js_modal(h,
    .trigger_id = "request-settings-close",
    .target_id = "settings-dialog",
    .return_value = "cancel",
    .action = SW_JS_MODAL_REQUEST_CLOSE
);
sw_js_modal(h,
    .trigger_id = "force-settings-close",
    .target_id = "settings-dialog",
    .return_value = "closed",
    .action = SW_JS_MODAL_CLOSE
);

SW_JS_MODAL_REQUEST_CLOSE fires the standard cancelable cancel event. SW_JS_MODAL_CLOSE closes immediately. Native method="dialog" forms need no JavaScript close binding, and their submitting button value becomes dialog.returnValue.

Use standard dialog events for application logic:

const dialog = document.getElementById('settings-dialog');

dialog.addEventListener('cancel', function (event) {
  if (hasUnsavedChanges()) event.preventDefault();
});

dialog.addEventListener('close', function () {
  console.log(dialog.returnValue);
});

Every dialog needs an accessible name through aria-labelledby or aria-label and a visible close control. Use autofocus when the browser's default initial focus is unsuitable. Backdrop dismissal is disabled unless close_on_backdrop is enabled.

Styling is application-owned:

dialog {
  max-width: 36rem;
  border: 1px solid #444;
  background: #222;
  color: #fff;
}

dialog::backdrop {
  background: rgb(0 0 0 / 65%);
}

Translations

One JSON object per source string:

{
  "Search": {
    "ar": "بحث",
    "ja": "検索"
  },
  "Language": {
    "ar": "اللغة",
    "ja": "言語"
  }
}
#include "sw_translator.h"

sw_translator* tr = sw_translator_create("resources/translations.json",
    .code = "en",
    .label = "English",
    .direction = SW_LANGUAGE_DIRECTION_LTR
);

sw_translator_add(tr, .code = "ar", .label = "Arabic", .direction = SW_LANGUAGE_DIRECTION_RTL);
sw_translator_add(tr, .code = "ja", .label = "Japanese", .direction = SW_LANGUAGE_DIRECTION_LTR);
sw_translator_set_language(tr, "ja");

English source text is the fallback. The installed file is share/syphax_web/translations.json.

Large multipart uploads save only to caller-provided paths; the caller owns those files and cleanup.

Examples

Run one example at a time. They all bind port 8000. Open HTTP examples at http://127.0.0.1:8000 and HTTPS examples at https://127.0.0.1:8000. For HTTPS examples, build with ./build.sh -tls. For the database example, build with ./build.sh -sqlite, ./build.sh -postgres, or ./build.sh -db.

  • bin/01_http: basic HTTP app; HTTPS in a TLS build
  • bin/02_https: HTTPS status page
  • bin/03_static_site: static HTTPS site
  • bin/04_live_queue: live form demo
  • bin/05_folder_app: folder-backed app
  • bin/06_session_login: HTTPS session login demo
  • bin/07_database: database-backed counter in database builds

Scope

Use Syphax-Web for embedded tools, local dashboards, internal services, and small server-rendered apps.

Use a front proxy for HTTP/2, compression, rate limits, and edge policy.

TLS listeners speak HTTP/1.1. Request bodies are bounded. Chunked request bodies are a future add.

License

MIT

Releases

Packages

Contributors

Languages