diff --git a/exmaples.c b/.arch/exmaples.c similarity index 78% rename from exmaples.c rename to .arch/exmaples.c index 946c4a8..77a69c1 100644 --- a/exmaples.c +++ b/.arch/exmaples.c @@ -2,6 +2,9 @@ #include #include +#include +#include +#include #define ARRAY_LEN(arr) (sizeof(arr) / sizeof((arr)[0])) @@ -25,7 +28,10 @@ void swagger_json_handler(chttpx_request_t* req, chttpx_response_t* res) void swagger_gui_handler(chttpx_request_t* req, chttpx_response_t* res) { - char* url = "http://localhost:80/api/v1"; + const char* port_env = getenv("CHTTPX_PORT"); + const char* port = port_env && port_env[0] != '\0' ? port_env : "8080"; + char url[128]; + snprintf(url, sizeof(url), "http://localhost:%s/api/v1", port); char swagger_html[8192]; snprintf(swagger_html, sizeof(swagger_html), @@ -153,12 +159,51 @@ void file_upload(chttpx_request_t* req, chttpx_response_t* res) return; } +void chat_on_open(chttpx_wsocket_t* ws, void* userdata) +{ + (void)userdata; + const char* room = cHTTPX_WSocketParam(ws, "room_id"); + char welcome[256]; + snprintf(welcome, sizeof(welcome), "Welcome to room %s!", room ? room : "?"); + cHTTPX_WSocketSend(ws, welcome); +} + +void chat_on_message(chttpx_wsocket_t* ws, const unsigned char* data, size_t len, int opcode, void* userdata) +{ + (void)opcode; + (void)userdata; + + char msg[4096]; + if (len >= sizeof(msg)) + len = sizeof(msg) - 1; + memcpy(msg, data, len); + msg[len] = '\0'; + + /* Only clients in the same room (same URL path) receive the message. */ + cHTTPX_WSocketBroadcastPeers(ws, msg); +} + +void chat_on_close(chttpx_wsocket_t* ws, void* userdata) +{ + (void)ws; + (void)userdata; +} + +static chttpx_wsocket_callbacks_t chat_callbacks = { + .on_open = chat_on_open, + .on_message = chat_on_message, + .on_close = chat_on_close, +}; + int main() { chttpx_serv_t serv = {0}; - size_t max_clients = 2; - if (cHTTPX_Init(&serv, 80, &max_clients) != 0) + const char* port_env = getenv("CHTTPX_PORT"); + uint16_t port = port_env ? (uint16_t)atoi(port_env) : 8080; + + size_t max_clients = 1024; + if (cHTTPX_Init(&serv, port, &max_clients) != 0) { printf("Failed to start server\n"); return 1; @@ -180,6 +225,8 @@ int main() cHTTPX_RegisterRoute(&v1, "PATCH", "/file", file_upload); cHTTPX_RegisterRoute(&v1, "GET", "/doc.api/swagger/json", swagger_json_handler); cHTTPX_RegisterRoute(&v1, "GET", "/doc.api/swagger/gui", swagger_gui_handler); + /* Client connects: ws://host/api/v1/ws/chat/lobby (room_id = "lobby") */ + cHTTPX_WSocketRegisterRoute(&v1, "/ws/chat/{room_id}", &chat_callbacks); /* At the very end, to start listening to incoming requests from users. */ cHTTPX_Listen(); diff --git a/prog.png b/.arch/prog.png similarity index 100% rename from prog.png rename to .arch/prog.png diff --git a/swagger.json b/.arch/swagger.json similarity index 100% rename from swagger.json rename to .arch/swagger.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ebab7d1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git +.github +.out +.build +*.tar.gz +libchttpx-dev +libchttpx.so +*.dll +*.a +logs/ +exmaples.c +swagger.json +DEBIAN/ +scripts/ +tools/ +*.md +LICENSE +ChangeLog diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2825d54..7d3a617 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,6 +19,8 @@ Use a **conventional commit** title — it becomes the release version on merge: - [ ] PR title follows the table above - [ ] Code builds on Linux (`make lin` and `make libchttpx.so`) +- [ ] Tests pass (`make test`) +- [ ] Example server builds separately (`make example`) if needed - [ ] Code builds on Windows (`make win`) if applicable - [ ] Source is formatted (`make lin-format` or `clang-format`) - [ ] README updated if public API or usage changed @@ -26,3 +28,6 @@ Use a **conventional commit** title — it becomes the release version on merge: ## Related issues + + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fc3c63..0c558b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,8 @@ name: CI on: push: - branches: [main, master] + branches: [develop, main, master] pull_request: - branches: [main, master] permissions: contents: read @@ -25,6 +24,9 @@ jobs: - name: Build binary run: make lin + - name: Run tests + run: make test + - name: Build shared library run: make libchttpx.so @@ -50,6 +52,9 @@ jobs: - name: Build run: mingw32-make win SHELL=cmd + - name: Run tests + run: mingw32-make win-test-run SHELL=cmd + static-analysis: name: Static analysis (cppcheck) runs-on: ubuntu-latest diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..4c5afa5 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,54 @@ +name: Docker + +on: + push: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: docker-${{ github.ref }} + cancel-in-progress: false + +jobs: + publish: + name: Build and push to Docker Hub + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: noneandundefined/libchttpx + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=sha,prefix= + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 9d50588..edcc349 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -16,6 +16,6 @@ jobs: uses: actions/checkout@v4 - name: Apply labels - uses: actions/labeler@v5 + uses: actions/labeler@v7 with: repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 0000000..b5e8db5 --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,128 @@ +name: PR Preview Build + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + preview-linux: + name: Preview build (Linux) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y gcc make libcjson-dev + + - name: Build preview archive + run: make lin-lib + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: libchttpx-dev-pr-${{ github.event.pull_request.number }} + path: libchttpx-dev.tar.gz + retention-days: 14 + + - name: Update PR with install instructions + uses: actions/github-script@v7 + env: + RUN_ID: ${{ github.run_id }} + PR_NUMBER: ${{ github.event.pull_request.number }} + HEAD_REF: ${{ github.head_ref }} + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + with: + script: | + const markerStart = ''; + const markerEnd = ''; + const runId = process.env.RUN_ID; + const prNumber = process.env.PR_NUMBER; + const headRef = process.env.HEAD_REF; + const headRepo = process.env.HEAD_REPO; + const { owner, repo } = context.repo; + const fullRepo = `${owner}/${repo}`; + const runUrl = `https://github.com/${fullRepo}/actions/runs/${runId}`; + const scriptUrl = `https://raw.githubusercontent.com/${headRepo}/${headRef}/scripts/install-preview.sh`; + const oneLiner = `curl -sL ${scriptUrl} | sudo bash -s -- ${runId} ${prNumber} ${fullRepo}`; + + const section = [ + markerStart, + '## libchttpx preview build (Linux)', + '', + '> Auto-generated by CI on every push. Artifact is kept for 14 days.', + '', + '| | |', + '|---|---|', + `| **PR** | #${prNumber} |`, + `| **Workflow run** | [${runId}](${runUrl}) |`, + `| **Artifact** | \`libchttpx-dev-pr-${prNumber}\` |`, + '', + '### One-liner install (Linux)', + '', + 'Requires [GitHub CLI](https://cli.github.com/) — run `gh auth login` once.', + '', + '```bash', + oneLiner, + '```', + '', + '### Manual install', + '', + '```bash', + `gh run download ${runId} -R ${fullRepo} -n libchttpx-dev-pr-${prNumber}`, + 'tar -xzf libchttpx-dev.tar.gz && cd libchttpx-dev', + 'sudo mkdir -p /usr/local/include/libchttpx /usr/local/lib/pkgconfig', + 'sudo cp -r include/* /usr/local/include/libchttpx/', + 'sudo cp libchttpx.so /usr/local/lib/', + 'sudo cp libchttpx.pc /usr/local/lib/pkgconfig/', + 'sudo ldconfig', + '```', + '', + '### Compile your app', + '', + '```bash', + 'gcc main.c $(pkg-config --cflags --libs libchttpx) -lcjson', + '```', + markerEnd, + ].join('\n'); + + const isFork = headRepo !== fullRepo; + const { data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: Number(prNumber), + }); + + if (isFork) { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: Number(prNumber), + body: section.replace(markerStart, '').replace(markerEnd, '').trim() + + '\n\n> Posted as comment because this PR is from a fork.', + }); + return; + } + + let body = pr.body ?? ''; + const startIdx = body.indexOf(markerStart); + const endIdx = body.indexOf(markerEnd); + + if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { + body = body.slice(0, startIdx) + section + body.slice(endIdx + markerEnd.length); + } else { + body = body.trimEnd() + '\n\n' + section; + } + + await github.rest.pulls.update({ + owner, + repo, + pull_number: Number(prNumber), + body, + }); diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6ac8213 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +# syntax=docker/dockerfile:1 + +# Dev base image: prebuilt libchttpx + toolchain for compiling C/C++ apps. +# +# Usage in other projects: +# FROM noneandundefined/libchttpx:latest +# WORKDIR /app +# COPY . . +# RUN gcc main.c $(pkg-config --cflags --libs libchttpx) -lcjson -o app + +FROM debian:bookworm-slim AS builder + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + libcjson-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src + +COPY Makefile libchttpx.pc ./ +COPY include/ include/ +COPY src/ src/ +COPY tests/ tests/ + +RUN make test \ + && make lib-install DESTDIR=/out PREFIX=/usr/local + +FROM debian:bookworm-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + libcjson-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /out/usr/local/ /usr/local/ + +RUN ldconfig \ + && pkg-config --exists libchttpx \ + && test -f /usr/local/lib/libchttpx.so \ + && test -d /usr/local/include/libchttpx + +LABEL org.opencontainers.image.source="https://github.com/netcorelink/libchttpx" \ + org.opencontainers.image.description="A powerful, cross-platform HTTP server library in C/C++ for building full-featured web servers." \ + org.opencontainers.image.licenses="MIT" + +WORKDIR /app diff --git a/Makefile b/Makefile index 99506b9..3133370 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,11 @@ -TARGET=chttpx-server +TARGET = libchttpx-tests +EXAMPLE_TARGET = chttpx-server RELEASE_DIR = libchttpx-dev TAR = $(RELEASE_DIR).tar.gz CC = gcc -CFLAGS = -Wall -Wextra -O2 -Iinclude +CFLAGS = -Wall -Wextra -Werror=implicit-function-declaration -O2 -Iinclude TARGET_DLL = libchttpx.dll CLANG_FORMAT = clang-format @@ -17,40 +18,65 @@ PKGDIR ?= /pkg/usr/local WIN_LIB_DIR = tools -LIN_LDFLAGS = -lcjson +LIN_LDFLAGS = -lcjson -lpthread WIN_LDFLAGS = -lws2_32 -LIN_SRCS = $(filter-out ./lib/cjson/cJSON.c, $(shell find . -name '*.c')) -WIN_SRCS = $(wildcard *.c) $(wildcard */*.c) $(wildcard */*/*.c) +LIB_SRCS = $(wildcard src/*.c) +LIB_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(LIB_SRCS)) -LIN_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(LIN_SRCS)) -WIN_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(WIN_SRCS)) +TEST_SRCS = $(wildcard tests/test_*.c) +TEST_OBJS = $(patsubst tests/%.c,$(OBJDIR)/tests/%.o,$(TEST_SRCS)) + +EXAMPLE_SRC = .arch/exmaples.c +EXAMPLE_OBJ = $(OBJDIR)/exmaples.o + +WIN_LIB_SRCS = $(wildcard src/*.c) lib/cjson/cJSON.c # LINux build # - -lin: $(BINDIR)/$(TARGET) +lin: $(BINDIR)/$(TARGET) libchttpx.so -$(BINDIR)/$(TARGET): $(LIN_OBJS) +$(BINDIR)/$(TARGET): $(LIB_OBJS) $(TEST_OBJS) @mkdir -p $(BINDIR) - $(CC) $(CFLAGS) -o $@ $(LIN_OBJS) $(LIN_LDFLAGS) + $(CC) $(CFLAGS) -o $@ $(LIB_OBJS) $(TEST_OBJS) $(LIN_LDFLAGS) $(OBJDIR)/%.o: %.c @mkdir -p $(dir $@) $(CC) $(CFLAGS) -fPIC -c $< -o $@ +$(OBJDIR)/tests/%.o: tests/%.c + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -fPIC -c $< -o $@ + +example: $(BINDIR)/$(EXAMPLE_TARGET) + +$(BINDIR)/$(EXAMPLE_TARGET): $(LIB_OBJS) $(EXAMPLE_OBJ) + @mkdir -p $(BINDIR) + $(CC) $(CFLAGS) -o $@ $(LIB_OBJS) $(EXAMPLE_OBJ) $(LIN_LDFLAGS) + # WINdows build # - +$(OBJDIR)/exmaples.o: $(EXAMPLE_SRC) + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -fPIC -c $< -o $@ + win: if not exist $(BINDIR) mkdir $(BINDIR) - $(CC) $(CFLAGS) -D_WIN32 -mconsole $(WIN_SRCS) -o $(BINDIR)/$(TARGET).exe $(WIN_LDFLAGS) + $(CC) $(CFLAGS) -D_WIN32 -mconsole $(WIN_LIB_SRCS) $(EXAMPLE_SRC) -o $(BINDIR)/$(EXAMPLE_TARGET).exe $(WIN_LDFLAGS) + +win-test: $(BINDIR)/$(TARGET).exe + +$(BINDIR)/$(TARGET).exe: + if not exist $(BINDIR) mkdir $(BINDIR) + $(CC) $(CFLAGS) -D_WIN32 -mconsole $(WIN_LIB_SRCS) $(TEST_SRCS) -o $@ $(WIN_LDFLAGS) # LINux shared library # - -libchttpx.so: $(LIN_OBJS) - $(CC) -shared -fPIC -o libchttpx.so $(LIN_OBJS) +libchttpx.so: $(LIB_OBJS) + $(CC) -shared -fPIC -o libchttpx.so $(LIB_OBJS) $(LIN_LDFLAGS) # LINux lib install # - @@ -67,9 +93,9 @@ lib-install: libchttpx.so # WINdows lib compile # - -win-lib: $(WIN_OBJS) +win-lib: @echo "Building Windows DLL..." - $(CC) -shared -o $(TARGET_DLL) $(WIN_OBJS) -Wl,--out-implib,libchttpx.a -lws2_32 + $(CC) -shared -o $(TARGET_DLL) $(WIN_LIB_SRCS) -Wl,--out-implib,libchttpx.a -lws2_32 @echo "Copying files to $(WIN_LIB_DIR)..." @mkdir -p $(WIN_LIB_DIR) @@ -106,13 +132,21 @@ lin-lib: clean libchttpx.so # LINux run # - -lin-run: lin +test: lin $(BINDIR)/$(TARGET) +lin-run: test + +lin-example: example + $(BINDIR)/$(EXAMPLE_TARGET) + # WINdows run # - win-run: win + $(BINDIR)\$(EXAMPLE_TARGET).exe + +win-test-run: win-test $(BINDIR)\$(TARGET).exe # LINux format @@ -120,16 +154,16 @@ win-run: win lin-format: @echo ">> Formatting clang source files" - $(CLANG_FORMAT) -i $(LIN_SRCS) + $(CLANG_FORMAT) -i $(LIB_SRCS) $(TEST_SRCS) # WINdows format # - win-format: @echo ">> Formatting clang source files" - $(CLANG_FORMAT) -i $(WIN_SRCS) + $(CLANG_FORMAT) -i $(WIN_LIB_SRCS) $(TEST_SRCS) $(EXAMPLE_SRC) -run: run-lin +run: lin-run clean: rm -rf $(OBJDIR) $(BINDIR) *.a *.dll diff --git a/README.md b/README.md index 31750bb..45e6fdd 100644 --- a/README.md +++ b/README.md @@ -1,229 +1,383 @@

- golangci-lint logo + libchttpx logo

netcorelink/libchttpx

-

A powerful, cross-platform HTTP server library in C/C++ for building full-featured web servers

+

Cross-platform HTTP/WebSocket server library in C/C++

--- -`netcorelink/libchttpx` a powerful, cross-platform HTTP server library in C/C++ for building full-featured web servers. +`libchttpx` — библиотека для HTTP-сервера на C/C++: маршруты, middleware, CORS, JSON-парсинг, WebSocket, загрузка файлов. -## Linux +**Зависимости:** `libcjson` (Linux: `libcjson-dev`, Windows: входит в zip-релиз). + +--- + +## Установка + +### Linux — последняя версия ```bash curl -s https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.sh | sudo sh ``` -## Windows +### Linux — конкретная версия + +```bash +curl -s https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.sh | sudo sh -s -- --version=1.5.5 +``` + +Также работает: `-v=1.5.5`, `-v 1.5.5`, `--version v1.5.5`. + +Локально: + +```bash +sudo bash scripts/install.sh --version=1.5.5 +sudo bash scripts/install.sh --help +``` + +Файлы ставятся в `/usr/local`: + +| Путь | Содержимое | +|------|------------| +| `/usr/local/include/libchttpx/` | заголовки | +| `/usr/local/lib/libchttpx.so` | shared library | +| `/usr/local/lib/pkgconfig/libchttpx.pc` | pkg-config | + +Проверка: + +```bash +pkg-config --modversion libchttpx +ldconfig -p | grep libchttpx +``` + +### Windows — последняя версия ```powershell iwr https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.ps1 -UseBasicParsing | iex ``` ---- +### Windows — конкретная версия -

Documentation

+```powershell +iwr https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.ps1 -OutFile install.ps1 +powershell -ExecutionPolicy Bypass -File install.ps1 --version=1.5.5 +``` -### Initial http server +Через pipe `| iex` аргументы не передаются — используйте переменную окружения: -```c -#include +```powershell +$env:LIBCHTTPX_VERSION = '1.5.5' +iwr https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.ps1 -UseBasicParsing | iex +``` -int main() -{ - chttpx_serv_t serv = {0}; +### Ручная установка из GitHub Releases - if (cHTTPX_Init(&serv, 80) != 0) { - printf("Failed to start server\n"); - return 1; - } +Список версий: https://github.com/netcorelink/libchttpx/releases - // cores - // middlewares - // routes +```bash +VERSION=v1.5.5 +curl -L "https://github.com/netcorelink/libchttpx/releases/download/${VERSION}/libchttpx-dev.tar.gz" -o libchttpx-dev.tar.gz +tar -xzf libchttpx-dev.tar.gz +cd libchttpx-dev +sudo cp -r include/* /usr/local/include/libchttpx/ +sudo cp libchttpx.so /usr/local/lib/ +sudo cp libchttpx.pc /usr/local/lib/pkgconfig/ +sudo ldconfig +``` - /* At the very end, to start listening to incoming requests from users. */ - cHTTPX_Listen(); -} +### Docker (base image для сборки приложений) + +```dockerfile +FROM noneandundefined/libchttpx:latest + +WORKDIR /app +COPY . . +RUN gcc main.c $(pkg-config --cflags --libs libchttpx) -lcjson -o app ``` -### Server timeouts settings +Образ содержит toolchain + установленную `libchttpx`. Тег `latest` обновляется при push в `main`. -```c -/* Timeouts */ -serv.read_timeout_sec = 300; -serv.write_timeout_sec = 300; -serv.idle_timeout_sec = 90; +> Для фиксации версии в production используйте tarball из [Releases](https://github.com/netcorelink/libchttpx/releases) или соберите образ из git-тега (`git checkout v1.5.5 && docker build -t libchttpx:1.5.5 .`). + +### Сборка из исходников + +```bash +git clone https://github.com/netcorelink/libchttpx.git +cd libchttpx +sudo apt install -y gcc make libcjson-dev # Debian/Ubuntu + +make libchttpx.so +sudo make lib-install PREFIX=/usr/local +sudo ldconfig +``` + +Архив для релиза: + +```bash +make lin-lib # → libchttpx-dev.tar.gz ``` -### CORS Settings +--- -`origins` – Array of allowed origin strings (e.g. "https://example.com"). Each origin must match exactly the value of the "Origin" header. +## Компиляция вашего приложения + +```bash +gcc main.c $(pkg-config --cflags --libs libchttpx) -lcjson -o server +``` -`origins_count` – Number of elements in the origins array. +Windows (после `install.ps1`): -`methods` – Comma-separated list of allowed HTTP methods. If NULL, defaults to: "GET, POST, PUT, DELETE, OPTIONS". +```powershell +gcc server.c -I"%ProgramFiles%\libchttpx\include" -L"%ProgramFiles%\libchttpx" -lchttpx -lws2_32 -o server.exe +``` -`headers` – Comma-separated list of allowed request headers. If NULL, defaults to: "Content-Type". +Подключение: ```c -/* Cors */ -const char *allowed_origins[] = { - "https://exmaple.ru", - "http://localhost:8080", -}; - -cHTTPX_Cors(allowed_origins, cHTTPX_ARRAY_LEN(allowed_origins), NULL, "Accept-Language, Auth"); +#include ``` -### Middlewares +--- -Example: Middleware for checking the authenticated user. +## Быстрый старт ```c -chttpx_middleware_result_t auth_middleware(chttpx_request_t *req, chttpx_response_t *res) { - const char *token = cHTTPX_Header(req, "Auth-Token"); +#include +#include - if (!token) { - *res = cHTTPX_ResJson(cHTTPX_StatusUnauthorized, "{\"error\": \"unauthorized\"}"); - return out; - } +void home(chttpx_request_t* req, chttpx_response_t* res) +{ + (void)req; + *res = cHTTPX_ResHtml(cHTTPX_StatusOK, "

Hello

"); +} + +int main(void) +{ + chttpx_serv_t serv = {0}; - return next; + size_t max_clients = 1024; + if (cHTTPX_Init(&serv, 8080, &max_clients) != 0) + return 1; + + chttpx_router_t api = cHTTPX_RoutePathPrefix("/api/v1"); + cHTTPX_RegisterRoute(&api, "GET", "/", home); + + cHTTPX_Listen(); + cHTTPX_Shutdown(); + return 0; } ``` -Middlewares are connected `before cHTTPX_Route`. +--- + +## Документация + +### Инициализация и лимиты ```c -cHTTPX_MiddlewareUse(auth_middleware); +chttpx_serv_t serv = {0}; +size_t max_clients = 1024; + +cHTTPX_Init(&serv, 8080, &max_clients); /* NULL вместо &max_clients → 255 по умолчанию */ + +serv.read_timeout_sec = 300; +serv.write_timeout_sec = 300; +serv.idle_timeout_sec = 90; ``` -### Routes +### CORS -`method` – HTTP method string, e.g., "GET", "POST". +```c +const char* allowed_origins[] = { + "https://example.com", + "http://localhost:8080", +}; -`path` – URL path to match, e.g., "/users". +cHTTPX_Cors(allowed_origins, 2, NULL, "Content-Type, Authorization"); +``` -`handler` – Function pointer to handle the request. The handler should return httpx_response_t. This allows the server to call the appropriate function when a matching request is received. +| Параметр | Описание | +|----------|----------| +| `origins` | Разрешённые Origin (точное совпадение) | +| `origins_count` | Количество элементов | +| `methods` | `NULL` → `GET, POST, PUT, DELETE, OPTIONS` | +| `headers` | `NULL` → `Content-Type` | + +### Middleware ```c -cHTTPX_Route("GET", "/", home_index); -cHTTPX_Route("GET", "/users/{uuid}/{org}", get_user); // ?org=netcorelink -cHTTPX_Route("POST", "/users", create_user); +chttpx_middleware_result_t auth_middleware(chttpx_request_t* req, chttpx_response_t* res) +{ + const char* token = cHTTPX_HeaderGet(req, "Authorization"); + if (!token) + { + *res = cHTTPX_ResJson(cHTTPX_StatusUnauthorized, "{\"error\":\"unauthorized\"}"); + return out; + } + return next; +} + +cHTTPX_MiddlewareRecovery(); +cHTTPX_MiddlewareLogging(); +cHTTPX_MiddlewareUse(auth_middleware); ``` -### Handlers +Встроенные: `cHTTPX_MiddlewareRecovery()`, `cHTTPX_MiddlewareLogging()`, `cHTTPX_MiddlewareRateLimiter(max, window_sec)`. -HTML page return. +### Маршруты + +Маршруты группируются префиксом: ```c -void home_index(chttpx_request_t *req, chttpx_response_t *res) { - *res = cHTTPX_ResHtml(cHTTPX_StatusOK, "

This is home page!

"); -} +chttpx_router_t v1 = cHTTPX_RoutePathPrefix("/api/v1"); + +cHTTPX_RegisterRoute(&v1, "GET", "/", home_index); +cHTTPX_RegisterRoute(&v1, "GET", "/users/{uuid}", get_user); +cHTTPX_RegisterRoute(&v1, "POST", "/users", create_user); ``` -### Http Response +Параметры пути: -Return Json response +```c +const char* uuid = cHTTPX_Param(req, "uuid"); +``` + +Query-параметры: ```c -return cHTTPX_ResJson(cHTTPX_StatusOK, "{\"message\": {\"uuid\": \"%s\", \"page\": \"%s\"}}", uuid, page); +const char* page = cHTTPX_Query(req, "page"); ``` -Return Html response -Return Media response +### Handlers и ответы - +```c +void home_index(chttpx_request_t* req, chttpx_response_t* res) +{ + (void)req; + *res = cHTTPX_ResHtml(cHTTPX_StatusOK, "

Home

"); +} -### Parsing JSON fields +*res = cHTTPX_ResJson(cHTTPX_StatusOK, "{\"ok\":true}"); +*res = cHTTPX_ResText(cHTTPX_StatusOK, "plain text"); +*res = cHTTPX_ResFile(cHTTPX_StatusOK, "/path/to/file.png"); +``` + +### JSON parsing ```c typedef struct { - char *uuid; - char *password; - int is_admin; + char* uuid; + char* password; + int is_admin; } user_t; -chttpx_response_t create_user(chttpx_request_t *req) { - user_t user = {0}; +void create_user(chttpx_request_t* req, chttpx_response_t* res) +{ + user_t user = {0}; - chttpx_validation_t fields[] = { - chttpx_validation_str("uuid", &user.uuid, true, 0, 36, VALIDATOR_NONE), - chttpx_validation_str("password", &user.password, true, 6, 16, VALIDATOR_NONE), - chttpx_validation_bool("is_admin", &user.is_admin, false), - }; + chttpx_validation_t fields[] = { + chttpx_validation_string("uuid", &user.uuid, true, 0, 36, VALIDATOR_NONE), + chttpx_validation_string("password", &user.password, true, 6, 16, VALIDATOR_NONE), + chttpx_validation_boolean("is_admin", &user.is_admin, false), + }; - if (!cHTTPX_Parse(req, fields, cHTTPX_ARRAY_LEN(fields), "en")) { - *res = cHTTPX_ResJson(cHTTPX_StatusBadRequest, "{\"error\": \"%s\"}", req->error_msg); - goto cleanup; - } + if (!cHTTPX_Parse(req, fields, ARRAY_LEN(fields))) + { + *res = cHTTPX_ResJson(cHTTPX_StatusBadRequest, "{\"error\":\"%s\"}", req->error_msg); + return; + } - /* ... */ + *res = cHTTPX_ResJson(cHTTPX_StatusCreated, "{\"uuid\":\"%s\"}", user.uuid); } ``` -> When working with cHTTPX_Parse, you need to refer to `req->error_msg`. +> Ошибки парсинга — в `req->error_msg`. Для i18n-сообщений: `cHTTPX_Validate(req, fields, ARRAY_LEN(fields), "en")`. -### Validations fields +### Заголовки и IP клиента -Validates an array of `cHTTPX_FieldValidation` structures. +```c +const char* origin = cHTTPX_HeaderGet(req, "Origin"); +const char* ip = cHTTPX_ClientIP(req); +``` -This function ensures that `required` fields are present, `string lengths` are within `limits`, -and basic validation for integers and boolean fields is performed. +### WebSocket -> When working with cHTTPX_Validate, you need to refer to `req->error_msg`. +Event-driven API: один poll-поток на все соединения. ```c -if (!cHTTPX_Validate(req, fields, cHTTPX_ARRAY_LEN(fields), "en")) { - *res = cHTTPX_ResJson(cHTTPX_StatusBadRequest, "{\"error\": \"%s\"}", req->error_msg); - goto cleanup; +void chat_on_open(chttpx_wsocket_t* ws, void* userdata) +{ + (void)userdata; + const char* room = cHTTPX_WSocketParam(ws, "room_id"); + cHTTPX_WSocketSend(ws, room ? room : "welcome"); } -``` -Example response by validation: +void chat_on_message(chttpx_wsocket_t* ws, const unsigned char* data, size_t len, int opcode, void* userdata) +{ + (void)opcode; + (void)userdata; -- Field password is required. -- Field password min length is 6. -- Field password max length is 16. + char msg[4096]; + if (len >= sizeof(msg)) + len = sizeof(msg) - 1; + memcpy(msg, data, len); + msg[len] = '\0'; -### Get Headers + /* Только клиенты в той же комнате (тот же URL) */ + cHTTPX_WSocketBroadcastPeers(ws, msg); +} -```c -const char *origin = cHTTPX_Header(req, "Origin"); -``` +void chat_on_close(chttpx_wsocket_t* ws, void* userdata) +{ + (void)ws; + (void)userdata; +} -cHTTPX_Header - Get a request header by name. +static chttpx_wsocket_callbacks_t chat_callbacks = { + .on_open = chat_on_open, + .on_message = chat_on_message, + .on_close = chat_on_close, +}; -`Parameters`: +chttpx_router_t v1 = cHTTPX_RoutePathPrefix("/api/v1"); +cHTTPX_WSocketRegisterRoute(&v1, "/ws/chat/{room_id}", &chat_callbacks); +``` -- req – Pointer to the HTTP request. -- name – Header name (case-insensitive). +Клиент подключается к комнате через URL: -### Get Params +``` +ws://localhost:8080/api/v1/ws/chat/lobby +``` -> The path must contain the /{uuid} construct. +| Функция | Назначение | +|---------|------------| +| `cHTTPX_WSocketSend(ws, text)` | Отправить одному клиенту | +| `cHTTPX_WSocketBroadcastPeers(ws, text)` | Всем в той же комнате | +| `cHTTPX_WSocketBroadcast(path, text)` | По полному пути | +| `cHTTPX_WSocketBroadcastRoom("room_id", "42", text)` | По параметру маршрута | +| `cHTTPX_WSocketParam(ws, "room_id")` | Параметр из URL | + +### Завершение работы ```c -const char *uuid = cHTTPX_Param(req, "uuid"); +cHTTPX_Listen(); /* блокирует, принимает соединения */ +cHTTPX_Shutdown(); /* освобождает ресурсы, закрывает WebSocket */ ``` -cHTTPX_Param - Get a route parameter value by its name. - -`Parameters`: +--- -- req – Pointer to the HTTP request. -- name – Name of the route parameter (e.g., "uuid"). +## Preview-сборки для PR -### Get Query params +При pull request CI публикует артефакт `libchttpx-dev-pr-N` (14 дней). Инструкция появляется в описании PR. -```c -const char *sizeParam = cHTTPX_Query(req, "size"); +```bash +gh run download RUN_ID -R netcorelink/libchttpx -n libchttpx-dev-pr-42 +tar -xzf libchttpx-dev.tar.gz && cd libchttpx-dev +# установка как в разделе «Ручная установка» ``` -cHTTPX_Query - Get a query parameter value by name. +--- -`Parameters`: +## Лицензия -- req – Pointer to the HTTP request. -- name – Name of the query parameter. +MIT — см. исходники репозитория. diff --git a/include/crosspltm.h b/include/crosspltm.h index 2e995f6..41fab23 100644 --- a/include/crosspltm.h +++ b/include/crosspltm.h @@ -6,14 +6,36 @@ extern "C" { #endif -#include - #if defined(_WIN32) || defined(_WIN64) #define CHTTPX_PLATFORM_WINDOWS #else #define CHTTPX_PLATFORM_POSIX #endif +#include + +/** Portable memmem — libc version needs _GNU_SOURCE; undeclared use truncates ptr on amd64. */ +static inline void* chttpx_memmem(const void* haystack, size_t haystacklen, const void* needle, size_t needlelen) +{ + if (needlelen == 0) + return (void*)haystack; + if (needlelen > haystacklen) + return NULL; + + const unsigned char* h = haystack; + const unsigned char* n = needle; + + for (size_t i = 0; i <= haystacklen - needlelen; i++) + { + if (h[i] == n[0] && memcmp(h + i, n, needlelen) == 0) + return (void*)(h + i); + } + + return NULL; +} + +#define memmem(haystack, haystacklen, needle, needlelen) chttpx_memmem(haystack, haystacklen, needle, needlelen) + #ifdef CHTTPX_PLATFORM_WINDOWS #define strdup _strdup #else @@ -66,29 +88,6 @@ typedef int chttpx_socket_t; #define strcasecmp _stricmp #endif -#ifdef CHTTPX_PLATFORM_WINDOWS - static void* memmem_win(const void* haystack, size_t haystacklen, const void* needle, size_t needlelen) - { - if (!needlelen) - return (void*)haystack; - if (needlelen > haystacklen) - return NULL; - - const unsigned char* h = haystack; - const unsigned char* n = needle; - - for (size_t i = 0; i <= haystacklen - needlelen; i++) - { - if (h[i] == n[0] && memcmp(h + i, n, needlelen) == 0) - return (void*)(h + i); - } - - return NULL; - } - -#define memmem(haystack, haystacklen, needle, needlelen) memmem_win(haystack, haystacklen, needle, needlelen) -#endif - #ifdef __cplusplus } #endif diff --git a/include/libchttpx.h b/include/libchttpx.h index 6fe7bdf..45a0991 100644 --- a/include/libchttpx.h +++ b/include/libchttpx.h @@ -37,6 +37,8 @@ extern "C" #include "i18n.h" +#include "websocket.h" + #ifdef __cplusplus } #endif diff --git a/include/params.h b/include/params.h index 4419efc..4288661 100644 --- a/include/params.h +++ b/include/params.h @@ -24,6 +24,12 @@ extern "C" */ const char* cHTTPX_Param(chttpx_request_t* req, const char* name); + /** + * Match a URL path against a route template (supports {param} segments). + * @return 1 if matched, 0 otherwise. + */ + int cHTTPX_MatchPath(const char* template, const char* path, chttpx_param_t* params, int* param_count); + #ifdef __cplusplus extern } diff --git a/include/serv.h b/include/serv.h index c05ad39..5ef15b2 100644 --- a/include/serv.h +++ b/include/serv.h @@ -19,10 +19,17 @@ extern "C" #include #include +#include -#define MAX_PATH 4096 +#define CHTTPX_MAX_PATH 4096 #define MAX_CLIENTS_DEFAULT 255 + typedef struct chttpx_wsocket chttpx_wsocket_t; + typedef void (*chttpx_wsocket_on_open_t)(chttpx_wsocket_t* ws, void* userdata); + typedef void (*chttpx_wsocket_on_message_t)(chttpx_wsocket_t* ws, const unsigned char* data, size_t len, + int opcode, void* userdata); + typedef void (*chttpx_wsocket_on_close_t)(chttpx_wsocket_t* ws, void* userdata); + /* Base struct route for library */ typedef struct { @@ -31,6 +38,15 @@ extern "C" chttpx_handler_t handler; } chttpx_route_t; + typedef struct + { + char* path; + chttpx_wsocket_on_open_t on_open; + chttpx_wsocket_on_message_t on_message; + chttpx_wsocket_on_close_t on_close; + void* userdata; + } chttpx_wsocket_route_entry_t; + typedef struct { uint16_t port; @@ -50,6 +66,11 @@ extern "C" size_t routes_count; size_t routes_capacity; + /* WebSocket routes */ + chttpx_wsocket_route_entry_t* ws_routes; + size_t ws_routes_count; + size_t ws_routes_capacity; + /* Middlewares */ chttpx_middleware_stack_t middleware; diff --git a/include/websocket.h b/include/websocket.h index acecf98..bb1b52e 100644 --- a/include/websocket.h +++ b/include/websocket.h @@ -5,9 +5,20 @@ * under the terms of the MIT license. See `libchttpx.c` for details. */ +#ifndef WEBSOCKET_H +#define WEBSOCKET_H + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include "crosspltm.h" +#include "request.h" #include "serv.h" -#include +#include +#include #define CHTTPX_WSOCKET_OPCODE_CONTINUATION 0x0 #define CHTTPX_WSOCKET_OPCODE_TEXT 0x1 @@ -16,38 +27,63 @@ #define CHTTPX_WSOCKET_OPCODE_PING 0x9 #define CHTTPX_WSOCKET_OPCODE_PONG 0xA -typedef struct -{ - /* FIN - final fragment - * 1 eq. this is the last frame of the message - * 0 eq. the message is divided into several parts + struct chttpx_wsocket + { + chttpx_socket_t socket; + int connected; + /** Full request path, e.g. /api/v1/ws/chat/lobby-42 */ + char path[CHTTPX_MAX_PATH]; + chttpx_param_t params[MAX_PARAMS]; + size_t params_count; + void* userdata; + }; + + typedef struct chttpx_wsocket chttpx_wsocket_t; + + typedef void (*chttpx_wsocket_on_open_t)(chttpx_wsocket_t* ws, void* userdata); + typedef void (*chttpx_wsocket_on_message_t)(chttpx_wsocket_t* ws, const unsigned char* data, size_t len, + int opcode, void* userdata); + typedef void (*chttpx_wsocket_on_close_t)(chttpx_wsocket_t* ws, void* userdata); + + typedef struct + { + chttpx_wsocket_on_open_t on_open; + chttpx_wsocket_on_message_t on_message; + chttpx_wsocket_on_close_t on_close; + void* userdata; + } chttpx_wsocket_callbacks_t; + + /** + * Register a WebSocket route with event callbacks (non-blocking, shared poll thread). */ - int fin; - /* Check define CHTTPX_WSOCKET_OPCODE */ - int opcode; - int masked; - /* Length data in payload */ - uint64_t payload_len; - /* Mask for XOR payload */ - unsigned char mask[4]; - /* Data in socket */ - unsigned char* payload; -} wsocket_frame_t; - -typedef struct -{ - int socket; - int connected; -} chttpx_wsocket_t; + void cHTTPX_WSocketRegisterRoute(chttpx_router_t* r, const char* path, const chttpx_wsocket_callbacks_t* callbacks); + + /** Send a text frame to one client. */ + int cHTTPX_WSocketSend(chttpx_wsocket_t* ws, const char* text); + + /** Send a binary frame to one client. */ + int cHTTPX_WSocketSendBinary(chttpx_wsocket_t* ws, const unsigned char* data, size_t len); + + /** Route param captured at connect time (e.g. room_id from /ws/chat/{room_id}). */ + const char* cHTTPX_WSocketParam(chttpx_wsocket_t* ws, const char* name); + + /** Broadcast text to all clients on the exact same path (same room URL). */ + int cHTTPX_WSocketBroadcast(const char* path, const char* text); -typedef void (*chttpx_wsocket_handler_t)(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len); + /** Broadcast text to everyone in the sender's room (same path as ws). */ + int cHTTPX_WSocketBroadcastPeers(chttpx_wsocket_t* ws, const char* text); -typedef void (*chttpx_wsocket_route_t)(chttpx_wsocket_t* wsocket); + /** Broadcast text to all clients whose route param matches (e.g. room_id = "42"). */ + int cHTTPX_WSocketBroadcastRoom(const char* param_name, const char* param_value, const char* text); -void cHTTPX_WSocketRegisterRoute(chttpx_router_t* r, const char* path, chttpx_wsocket_route_t handler); + /** Stop the WebSocket engine and close all connections. */ + void cHTTPX_WSocketShutdown(void); -int cHTTPX_WSocketUpgrade(int client_socket, const char* sec_wsocket_key); + /** Internal: handle upgrade request. Returns 1 if WebSocket took ownership of the socket. */ + int cHTTPX_WSocketTryHandle(chttpx_request_t* req); -int cHTTPX_WSocketSend(chttpx_wsocket_t* wsocket, const unsigned char* data, size_t len); +#ifdef __cplusplus +} +#endif -int cHTTPX_WSocketRecv(chttpx_wsocket_t* wsocket, unsigned char* buffer, size_t len); \ No newline at end of file +#endif diff --git a/scripts/install-preview.sh b/scripts/install-preview.sh new file mode 100644 index 0000000..27b7530 --- /dev/null +++ b/scripts/install-preview.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Install libchttpx from a PR preview build (GitHub Actions artifact). +# +# Usage: +# curl -sL https://raw.githubusercontent.com/OWNER/REPO/BRANCH/scripts/install-preview.sh | sudo bash -s -- RUN_ID PR_NUMBER [REPO] +# +# Example: +# curl -sL https://raw.githubusercontent.com/netcorelink/libchttpx/feat/ws/scripts/install-preview.sh | sudo bash -s -- 1234567890 42 +# +# Requires: gh (GitHub CLI) authenticated — run `gh auth login` once. + +set -e + +RUN_ID="${1:?Usage: install-preview.sh RUN_ID PR_NUMBER [REPO]}" +PR_NUMBER="${2:?Usage: install-preview.sh RUN_ID PR_NUMBER [REPO]}" +REPO="${3:-netcorelink/libchttpx}" + +PREFIX="${PREFIX:-/usr/local}" +ARTIFACT_NAME="libchttpx-dev-pr-${PR_NUMBER}" + +if [ "$(id -u)" -ne 0 ]; then + echo "Warning: it's recommended to run with sudo to install into $PREFIX" +fi + +if ! command -v gh >/dev/null 2>&1; then + echo "Error: GitHub CLI (gh) is required." + echo "Install: https://cli.github.com/" + echo "Then authenticate: gh auth login" + exit 1 +fi + +if ! gh auth status >/dev/null 2>&1; then + echo "Error: gh is not authenticated. Run: gh auth login" + exit 1 +fi + +if pkg-config --exists cjson; then + echo "cjson already installed." +else + echo "cjson not found. Installing..." + + if command -v apt >/dev/null 2>&1; then + sudo apt install -y libcjson-dev + elif command -v pacman >/dev/null 2>&1; then + sudo pacman -Sy --noconfirm cjson + elif command -v dnf >/dev/null 2>&1; then + sudo dnf install -y cjson-devel + elif command -v zypper >/dev/null 2>&1; then + sudo zypper install -y cjson-devel + else + echo "Unsupported package manager. Install cjson manually." + exit 1 + fi +fi + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "Downloading preview build (run $RUN_ID, PR #$PR_NUMBER)..." +cd "$TMPDIR" +gh run download "$RUN_ID" -R "$REPO" -n "$ARTIFACT_NAME" + +echo "Extracting..." +tar -xzf libchttpx-dev.tar.gz +cd libchttpx-dev + +echo "Installing headers..." +mkdir -p "$PREFIX/include/libchttpx" +cp -r include/* "$PREFIX/include/libchttpx" + +echo "Installing shared library..." +mkdir -p "$PREFIX/lib" +cp libchttpx.so "$PREFIX/lib/" + +echo "Installing pkg-config file..." +mkdir -p "$PREFIX/lib/pkgconfig" +cp libchttpx.pc "$PREFIX/lib/pkgconfig/" + +if command -v ldconfig >/dev/null 2>&1; then + echo "Updating library cache..." + ldconfig +fi + +echo "Preview libchttpx from PR #$PR_NUMBER installed successfully!" +echo "Use it via:" +echo " gcc main.c \$(pkg-config --cflags --libs libchttpx) -lcjson" diff --git a/scripts/install.ps1 b/scripts/install.ps1 index f45594f..a6fbc6c 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,13 +1,94 @@ +# Install libchttpx from GitHub Releases (Windows). +# +# Latest: +# iwr https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.ps1 -UseBasicParsing | iex +# +# Specific version: +# iwr .../install.ps1 -UseBasicParsing | iex --% --version=1.5.5 +# powershell -ExecutionPolicy Bypass -File install.ps1 --version=1.5.5 +# powershell -ExecutionPolicy Bypass -File install.ps1 -v=1.5.5 +# powershell -ExecutionPolicy Bypass -File install.ps1 -Version 1.5.5 +# $env:LIBCHTTPX_VERSION='1.5.5'; iwr .../install.ps1 -UseBasicParsing | iex + +param( + [Alias('v')] + [string]$Version +) + +function Show-InstallUsage { + Write-Host @" +Usage: install.ps1 [options] + +Options: + -Version, -v VER Release tag or version (e.g. 1.5.5 or v1.5.5) + --version=VER, -v=VER Same as above + --version VER Same as above + -h, --help Show this help + +Environment: + LIBCHTTPX_VERSION Version when piping to iex (e.g. 1.5.5) + +Examples: + iwr .../install.ps1 -UseBasicParsing | iex + powershell -File install.ps1 --version=1.5.5 + `$env:LIBCHTTPX_VERSION='1.5.5'; iwr .../install.ps1 -UseBasicParsing | iex +"@ +} + +function Normalize-VersionTag([string]$Ver) { + if ($Ver -match '^v') { return $Ver } + return "v$Ver" +} + +$i = 0 +while ($i -lt $args.Count) { + $arg = $args[$i] + switch -Regex ($arg) { + '^(-h|--help)$' { + Show-InstallUsage + exit 0 + } + '^(--version|-v)=(.+)$' { + $Version = $Matches[2] + } + '^(--version|-v)$' { + $i++ + if ($i -ge $args.Count) { + Write-Error "Error: $arg requires a value" + exit 1 + } + $Version = $args[$i] + } + default { + Write-Error "Error: unknown option: $arg" + Show-InstallUsage + exit 1 + } + } + $i++ +} + +if (-not $Version -and $env:LIBCHTTPX_VERSION) { + $Version = $env:LIBCHTTPX_VERSION +} + $tmp = "$env:TEMP\libchttpx.zip" $installDir = "$env:ProgramFiles\libchttpx" -$url = "https://github.com/netcorelink/libchttpx/releases/latest/download/libchttpx-win64.zip" -Write-Host "Installing libchttpx to $installDir" +if ($Version) { + $tag = Normalize-VersionTag $Version + $url = "https://github.com/netcorelink/libchttpx/releases/download/$tag/libchttpx-win64.zip" + Write-Host "Installing libchttpx $tag to $installDir" +} +else { + $url = "https://github.com/netcorelink/libchttpx/releases/latest/download/libchttpx-win64.zip" + Write-Host "Installing libchttpx (latest release) to $installDir" +} Invoke-WebRequest $url -OutFile $tmp Expand-Archive $tmp $installDir -Force +Remove-Item $tmp -Force -ErrorAction SilentlyContinue -# Set environment variables [Environment]::SetEnvironmentVariable( "CPATH", "$installDir\include;$installDir\lib\cjson", diff --git a/scripts/install.sh b/scripts/install.sh index f435536..c067907 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,24 +1,94 @@ #!/bin/bash -# Usage: curl -s https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.sh | sudo sh +# Install libchttpx from GitHub Releases. +# +# Latest: +# curl -s https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.sh | sudo sh +# +# Specific version: +# curl -s https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.sh | sudo sh -s -- --version=1.5.5 +# curl -s https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.sh | sudo sh -s -- -v=1.5.5 +# curl -s https://raw.githubusercontent.com/netcorelink/libchttpx/main/scripts/install.sh | sudo sh -s -- --version v1.5.5 set -e PREFIX="/usr/local" +VERSION="" RELEASE_URL="https://github.com/netcorelink/libchttpx/releases/latest/download/libchttpx-dev.tar.gz" -# root +usage() { + cat <<'EOF' +Usage: install.sh [options] + +Options: + --version=VER, -v=VER Release tag or version (e.g. 1.5.5 or v1.5.5) + --version VER, -v VER Same as above + --prefix=PATH Install prefix (default: /usr/local) + -h, --help Show this help + +Examples: + curl -s .../install.sh | sudo sh + curl -s .../install.sh | sudo sh -s -- --version=1.5.5 + curl -s .../install.sh | sudo sh -s -- -v=v1.5.5 +EOF +} + +normalize_version_tag() { + case "$1" in + v*) echo "$1" ;; + *) echo "v$1" ;; + esac +} + +while [ $# -gt 0 ]; do + case "$1" in + --version=*) + VERSION="${1#*=}" + ;; + -v=*) + VERSION="${1#*=}" + ;; + --version|-v) + if [ -z "${2:-}" ]; then + echo "Error: $1 requires a value" >&2 + exit 1 + fi + VERSION="$2" + shift + ;; + --prefix=*) + PREFIX="${1#*=}" + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Error: unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + esac + shift +done + +if [ -n "$VERSION" ]; then + TAG="$(normalize_version_tag "$VERSION")" + RELEASE_URL="https://github.com/netcorelink/libchttpx/releases/download/${TAG}/libchttpx-dev.tar.gz" + echo "Installing libchttpx ${TAG}..." +else + echo "Installing libchttpx (latest release)..." +fi + if [ "$(id -u)" -ne 0 ]; then - echo "Warning: it's recommended to run with sudo to install info $PREFIX" + echo "Warning: it's recommended to run with sudo to install into $PREFIX" fi -# cjson if pkg-config --exists cjson; then echo "cjson already installed." else - echo "cjson not found. Installing...." + echo "cjson not found. Installing..." if command -v apt >/dev/null 2>&1; then - # sudo apt update sudo apt install -y libcjson-dev elif command -v pacman >/dev/null 2>&1; then sudo pacman -Sy --noconfirm cjson @@ -35,30 +105,26 @@ fi echo "cjson installed successfully!" TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT -echo "Bulding in $TMPDIR" +echo "Building in $TMPDIR" cd "$TMPDIR" echo "Downloading libchttpx release..." -curl -L "$RELEASE_URL" -o libchttpx.tar.gz +curl -fsSL "$RELEASE_URL" -o libchttpx.tar.gz echo "Extracting..." tar -xzf libchttpx.tar.gz cd libchttpx-* -mkdir -p .build .out - -# Install headers -echo "Installing headers...." +echo "Installing headers..." mkdir -p "$PREFIX/include/libchttpx" cp -r include/* "$PREFIX/include/libchttpx" -# Install library -echo "Installing shared library...." +echo "Installing shared library..." mkdir -p "$PREFIX/lib" cp libchttpx.so "$PREFIX/lib/" -# Install pkg-config file echo "Installing pkg-config file..." mkdir -p "$PREFIX/lib/pkgconfig" cp libchttpx.pc "$PREFIX/lib/pkgconfig/" @@ -70,7 +136,4 @@ fi echo "libchttpx installed successfully!" echo "Use it via:" -echo " gcc main.c \$(pkg-config --cflags --libs libchttpx)" - -cd / -rm -rf "$TMPDIR" \ No newline at end of file +echo " gcc main.c \$(pkg-config --cflags --libs libchttpx) -lcjson" diff --git a/src/body.c b/src/body.c index e2b0a6d..e5a05a9 100644 --- a/src/body.c +++ b/src/body.c @@ -44,7 +44,8 @@ void _parse_req_body(chttpx_request_t* req, chttpx_socket_t client_fd, char* buf req->content_length = 0; } - int is_json_or_text = req->content_type && (strstr(req->content_type, "application/json") || strstr(req->content_type, "text/")); + int is_json_or_text = req->content_type[0] != '\0' && + (strstr(req->content_type, "application/json") || strstr(req->content_type, "text/")); const char* body_start = memmem(buffer, buffer_len, "\r\n\r\n", 4); if (!body_start || req->content_length == 0) diff --git a/src/media.c b/src/media.c index e22da0a..7d90650 100644 --- a/src/media.c +++ b/src/media.c @@ -88,10 +88,10 @@ static const content_type_map_t content_type_map[] = {{cHTTPX_CTYPE_HTML, ".html /* Parse media in request */ void _parse_media(chttpx_request_t* req, char* buffer, size_t buffer_len) { - if (!req->content_type) + if (req->content_type[0] == '\0') return; - if (req->content_length > 0 && (!req->content_type || !strstr(req->content_type, cHTTPX_CTYPE_JSON))) + if (req->content_length > 0 && !strstr(req->content_type, cHTTPX_CTYPE_JSON)) { char tmp_filename[512]; if (_save_body_to_temp_file(req, buffer, buffer_len, tmp_filename, sizeof(tmp_filename)) != 0) @@ -112,7 +112,7 @@ static int _save_body_to_temp_file(chttpx_request_t* req, char* initial_buffer, { const char* ext = content_type_to_ext(req->content_type); - snprintf(tmp_filename, tmp_filename_size, "/tmp/upload_%ld_%d%s", time(NULL), rand(), ext); + snprintf(tmp_filename, tmp_filename_size, "/tmp/upload_%lld_%d%s", (long long)time(NULL), rand(), ext); FILE* f = fopen(tmp_filename, "wb"); if (!f) return 1; diff --git a/src/params.c b/src/params.c index da068c4..1db54a8 100644 --- a/src/params.c +++ b/src/params.c @@ -24,6 +24,58 @@ #include "crosspltm.h" +#include + +int cHTTPX_MatchPath(const char* template, const char* path, chttpx_param_t* params, int* param_count) +{ + int count = 0; + const char* t = template; + const char* p = path; + + while (*t && *p) + { + if (*t == '{') + { + const char* t_end = strchr(t, '}'); + if (!t_end) + return 0; + + if (count >= MAX_PARAMS) + return 0; + + size_t name_len = (size_t)(t_end - t - 1); + if (name_len >= MAX_PARAM_NAME) + name_len = MAX_PARAM_NAME - 1; + strncpy(params[count].name, t + 1, name_len); + params[count].name[name_len] = 0; + + const char* slash = strchr(p, '/'); + size_t val_len = slash ? (size_t)(slash - p) : strlen(p); + if (val_len >= MAX_PARAM_VALUE) + val_len = MAX_PARAM_VALUE - 1; + strncpy(params[count].value, p, val_len); + params[count].value[val_len] = 0; + + count++; + t = t_end + 1; + p += val_len; + } + else + { + if (*t != *p) + return 0; + t++; + p++; + } + } + + if (*t || *p) + return 0; + + *param_count = count; + return 1; +} + /** * Get a route parameter value by its name. * @param req Pointer to the current HTTP request structure. diff --git a/src/response.c b/src/response.c index c60b53b..343531e 100644 --- a/src/response.c +++ b/src/response.c @@ -30,68 +30,15 @@ #include "headers.h" #include "cookies.h" #include "queries.h" +#include "params.h" #include "crosspltm.h" +#include "websocket.h" #include #include chttpx_response_t cHTTPX_ResJson(uint16_t status, const char* fmt, ...); -static int match_route(const char* template, const char* path, chttpx_param_t* params, int* param_count) -{ - int count = 0; - const char* t = template; - const char* p = path; - - while (*t && *p) - { - if (*t == '{') - { - const char* t_end = strchr(t, '}'); - if (!t_end) - return 0; - - if (count >= MAX_PARAMS) - return 0; - - size_t name_len = t_end - t - 1; - strncpy(params[count].name, t + 1, name_len); - params[count].name[name_len] = 0; - - const char* slash = strchr(p, '/'); - - size_t val_len = slash ? (size_t)(slash - p) : strlen(p); - - if (val_len >= MAX_PARAM_VALUE) - val_len = MAX_PARAM_VALUE - 1; - strncpy(params[count].value, p, val_len); - params[count].value[val_len] = 0; - - count++; - t = t_end + 1; - p += val_len; - } - else - { - if (*t != *p) - return 0; - t++; - p++; - } - } - - if (*t || *p) - return 0; - *param_count = count; - - return 1; -} - -/** - * Find a registered route by HTTP method and path. - * @param req Pointer to the current HTTP request structure. - * @return Pointer to the matching chttpx_route_t if found, NULL otherwise. - */ static chttpx_route_t* find_route(chttpx_request_t* req) { if (!serv) @@ -106,7 +53,7 @@ static chttpx_route_t* find_route(chttpx_request_t* req) continue; int count = 0; - if (match_route(serv->routes[i].path, req->path, req->params, &count)) + if (cHTTPX_MatchPath(serv->routes[i].path, req->path, req->params, &count)) { req->params_count = count; return &serv->routes[i]; @@ -310,7 +257,7 @@ static chttpx_request_t* parse_req_buffer(chttpx_socket_t client_fd, char* buffe buffer[received] = '\0'; - char method[16], path[MAX_PATH]; + char method[16], path[CHTTPX_MAX_PATH]; if (sscanf(buffer, "%15s %4095s", method, path) != 2) { @@ -403,6 +350,23 @@ void* chttpx_handle(void* arg) /* ALLOWED OPTIONS METHOD */ is_method_options(req); + int ws_result = cHTTPX_WSocketTryHandle(req); + int keep_socket = 0; + if (ws_result == 1) + { + keep_socket = 1; + goto cleanup; + } + + if (ws_result == -1) + { + chttpx_response_t res = + cHTTPX_ResJson(cHTTPX_StatusBadRequest, "{\"error\": \"websocket upgrade failed\"}"); + + send_response(req, res); + goto cleanup; + } + chttpx_route_t* r = find_route(req); chttpx_response_t res = {0}; @@ -462,7 +426,8 @@ void* chttpx_handle(void* arg) free(req->query); free(req); - chttpx_close(client_sock); + if (!keep_socket) + chttpx_close(client_sock); return NULL; } diff --git a/src/serv.c b/src/serv.c index 534c32e..69ffbb0 100644 --- a/src/serv.c +++ b/src/serv.c @@ -25,6 +25,7 @@ #include "utils.h" #include "crosspltm.h" #include "middlewares.h" +#include "websocket.h" /* Extern server struct data */ chttpx_serv_t* serv = NULL; @@ -102,6 +103,10 @@ int cHTTPX_Init(chttpx_serv_t* serv_p, uint16_t port, void* max_clients) serv->routes_count = 0; serv->routes_capacity = 0; + serv->ws_routes = NULL; + serv->ws_routes_count = 0; + serv->ws_routes_capacity = 0; + printf("HTTP server started on port %d...\n", port); return 0; @@ -159,7 +164,7 @@ void cHTTPX_RegisterRoute(chttpx_router_t* r, const char* method, const char* pa if (!r || !r->serv || !method || !path || !handler) return; - char fpath[MAX_PATH]; + char fpath[CHTTPX_MAX_PATH]; if (snprintf(fpath, sizeof(fpath), "%s%s", r->prefix, path) >= (int)sizeof(fpath)) return; @@ -197,7 +202,11 @@ void cHTTPX_Listen() continue; chttpx_socket_t client_fd = accept(serv->server_fd, NULL, NULL); +#ifdef _WIN32 + if (client_fd == INVALID_SOCKET) +#else if (client_fd < 0) +#endif continue; /* Inc. max clients */ @@ -239,6 +248,16 @@ void cHTTPX_Shutdown() serv->routes = NULL; serv->routes_count = 0; serv->routes_capacity = 0; + + for (size_t i = 0; i < serv->ws_routes_count; i++) + free(serv->ws_routes[i].path); + + free(serv->ws_routes); + serv->ws_routes = NULL; + serv->ws_routes_count = 0; + serv->ws_routes_capacity = 0; + + cHTTPX_WSocketShutdown(); #ifdef _WIN32 chttpx_close(serv->server_fd); #else diff --git a/src/websocket.c b/src/websocket.c index 55bbc5e..31ea394 100644 --- a/src/websocket.c +++ b/src/websocket.c @@ -1,58 +1,811 @@ /* * Copyright (c) 2026 netcorelink * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. + * WebSocket engine: one poll thread handles all connections (scalable for chat). */ #include "websocket.h" -int cHTTPX_WSocketUpgrade(int client_socket, const char* sec_wsocket_key) +#include "headers.h" +#include "params.h" +#include "utils.h" + +#ifndef CHTTPX_PLATFORM_WINDOWS +#include +#include +#include +#include +#include +#else +#include +#endif + +#include +#include +#include + +#define CHTTPX_WSOCKET_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" +#define CHTTPX_WSOCKET_MAX_PAYLOAD (64 * 1024) +#define CHTTPX_WSOCKET_READ_BUF (CHTTPX_WSOCKET_MAX_PAYLOAD + 16) + +typedef struct +{ + chttpx_wsocket_t public_ws; + chttpx_wsocket_on_open_t on_open; + chttpx_wsocket_on_message_t on_message; + chttpx_wsocket_on_close_t on_close; + void* route_userdata; + + unsigned char read_buf[CHTTPX_WSOCKET_READ_BUF]; + size_t read_len; + size_t frame_offset; +} ws_connection_t; + +typedef struct +{ + ws_connection_t* items; + size_t count; + size_t capacity; + thread_t poll_thread; + int engine_running; + int shutdown_requested; +#if defined(_WIN32) || defined(_WIN64) + CRITICAL_SECTION lock; +#else + pthread_mutex_t lock; +#endif +} ws_engine_t; + +static ws_engine_t ws_engine = {0}; + +static void ws_lock(void) +{ +#if defined(_WIN32) || defined(_WIN64) + EnterCriticalSection(&ws_engine.lock); +#else + pthread_mutex_lock(&ws_engine.lock); +#endif +} + +static void ws_unlock(void) +{ +#if defined(_WIN32) || defined(_WIN64) + LeaveCriticalSection(&ws_engine.lock); +#else + pthread_mutex_unlock(&ws_engine.lock); +#endif +} + +/* --- SHA-1 + Base64 (handshake) --- */ + +typedef struct +{ + uint32_t state[5]; + uint32_t count[2]; + unsigned char buffer[64]; +} sha1_ctx_t; + +static void sha1_transform(uint32_t state[5], const unsigned char buffer[64]) +{ + uint32_t w[80]; + uint32_t a, b, c, d, e, temp; + int i; + + for (i = 0; i < 16; i++) + w[i] = ((uint32_t)buffer[i * 4] << 24) | ((uint32_t)buffer[i * 4 + 1] << 16) | + ((uint32_t)buffer[i * 4 + 2] << 8) | (uint32_t)buffer[i * 4 + 3]; + for (i = 16; i < 80; i++) + w[i] = ((w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]) << 1) | + ((w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]) >> 31); + + a = state[0]; + b = state[1]; + c = state[2]; + d = state[3]; + e = state[4]; + + for (i = 0; i < 80; i++) + { + if (i < 20) + temp = ((b & c) | ((~b) & d)) + 0x5A827999; + else if (i < 40) + temp = (b ^ c ^ d) + 0x6ED9EBA1; + else if (i < 60) + temp = ((b & c) | (b & d) | (c & d)) + 0x8F1BBCDC; + else + temp = (b ^ c ^ d) + 0xCA62C1D6; + temp += ((a << 5) | (a >> 27)) + e + w[i]; + e = d; + d = c; + c = ((b << 30) | (b >> 2)); + b = a; + a = temp; + } + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + state[4] += e; +} + +static void sha1_init(sha1_ctx_t* ctx) +{ + ctx->state[0] = 0x67452301; + ctx->state[1] = 0xEFCDAB89; + ctx->state[2] = 0x98BADCFE; + ctx->state[3] = 0x10325476; + ctx->state[4] = 0xC3D2E1F0; + ctx->count[0] = ctx->count[1] = 0; +} + +static void sha1_update(sha1_ctx_t* ctx, const unsigned char* data, size_t len) { - char accept_key[128] = {0}; - char buffer[256]; + size_t i = 0; + size_t j = (ctx->count[0] >> 3) & 63; + + ctx->count[0] += (uint32_t)(len << 3); + if (ctx->count[0] < (uint32_t)(len << 3)) + ctx->count[1]++; + ctx->count[1] += (uint32_t)(len >> 29); - char key_concat[128]; - snprintf(key_concat, sizeof(key_concat), "%s258EAFA5-E914-47DA-95CA-C5AB0DC85B11", sec_wsocket_key); + if (j + len > 63) + { + memcpy(&ctx->buffer[j], data, 64 - j); + sha1_transform(ctx->state, ctx->buffer); + i = 64 - j; + while (i + 63 < len) + { + sha1_transform(ctx->state, &data[i]); + i += 64; + } + j = 0; + } + memcpy(&ctx->buffer[j], &data[i], len - i); +} + +static void sha1_final(sha1_ctx_t* ctx, unsigned char digest[20]) +{ + unsigned char finalcount[8]; + int i; - // unsigned char hash[20]; - // sha1((unsigned char*)key_concat, strlen(key_concat), hash); + for (i = 0; i < 8; i++) + finalcount[i] = (unsigned char)((ctx->count[(i >= 4) ? 1 : 0] >> ((3 - (i & 3)) * 8)) & 255); - // base64_encode(hash, 20, accept_key); + sha1_update(ctx, (unsigned char*)"\200", 1); + while ((ctx->count[0] & 504) != 448) + sha1_update(ctx, (unsigned char*)"\0", 1); + sha1_update(ctx, finalcount, 8); + for (i = 0; i < 20; i++) + digest[i] = (unsigned char)((ctx->state[i >> 2] >> ((3 - (i & 3)) * 8)) & 255); +} + +static void base64_encode(const unsigned char* input, size_t input_len, char* output) +{ + static const char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t i = 0, j = 0; + + while (i < input_len) + { + uint32_t a = i < input_len ? input[i++] : 0; + uint32_t b = i < input_len ? input[i++] : 0; + uint32_t c = i < input_len ? input[i++] : 0; + uint32_t triple = (a << 16) | (b << 8) | c; + output[j++] = table[(triple >> 18) & 0x3F]; + output[j++] = table[(triple >> 12) & 0x3F]; + output[j++] = table[(triple >> 6) & 0x3F]; + output[j++] = table[triple & 0x3F]; + } + output[j] = '\0'; + if (input_len % 3 == 1) + { + output[j - 2] = '='; + output[j - 1] = '='; + } + else if (input_len % 3 == 2) + output[j - 1] = '='; +} + +static void ws_compute_accept_key(const char* sec_key, char* accept_out, size_t accept_size) +{ + char combined[256]; + unsigned char hash[20]; + sha1_ctx_t ctx; + + snprintf(combined, sizeof(combined), "%s%s", sec_key, CHTTPX_WSOCKET_GUID); + sha1_init(&ctx); + sha1_update(&ctx, (unsigned char*)combined, strlen(combined)); + sha1_final(&ctx, hash); + if (accept_size >= 29) + base64_encode(hash, 20, accept_out); +} + +static int ws_set_nonblocking(chttpx_socket_t fd) +{ +#ifdef CHTTPX_PLATFORM_WINDOWS + u_long mode = 1; + return ioctlsocket(fd, FIONBIO, &mode); +#else + int flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) + return -1; + return fcntl(fd, F_SETFL, flags | O_NONBLOCK); +#endif +} + +static ssize_t ws_send_all(chttpx_socket_t fd, const unsigned char* buf, size_t len) +{ + size_t total = 0; + while (total < len) + { + ssize_t n = send(fd, (const char*)buf + total, (int)(len - total), 0); + if (n <= 0) + return n; + total += (size_t)n; + } + return (ssize_t)total; +} + +static int ws_send_frame(chttpx_socket_t fd, int opcode, const unsigned char* data, size_t len, int fin) +{ + unsigned char header[10]; + size_t header_len = 2; + + header[0] = (unsigned char)((fin ? 0x80 : 0) | (opcode & 0x0F)); + if (len <= 125) + header[1] = (unsigned char)len; + else if (len <= 65535) + { + header[1] = 126; + header[2] = (unsigned char)((len >> 8) & 0xFF); + header[3] = (unsigned char)(len & 0xFF); + header_len = 4; + } + else + return -1; + + if (ws_send_all(fd, header, header_len) < 0) + return -1; + if (len > 0 && data && ws_send_all(fd, data, len) < 0) + return -1; + return (int)len; +} + +int cHTTPX_WSocketSend(chttpx_wsocket_t* ws, const char* text) +{ + if (!ws || !ws->connected || !text) + return -1; + return ws_send_frame(ws->socket, CHTTPX_WSOCKET_OPCODE_TEXT, (const unsigned char*)text, strlen(text), 1); +} + +int cHTTPX_WSocketSendBinary(chttpx_wsocket_t* ws, const unsigned char* data, size_t len) +{ + if (!ws || !ws->connected) + return -1; + return ws_send_frame(ws->socket, CHTTPX_WSOCKET_OPCODE_BINARY, data, len, 1); +} + +static int ws_do_upgrade(chttpx_socket_t client_socket, const char* sec_websocket_key) +{ + char accept_key[64] = {0}; + char buffer[512]; + + ws_compute_accept_key(sec_websocket_key, accept_key, sizeof(accept_key)); int n = snprintf(buffer, sizeof(buffer), "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: %s\r\n\r\n", accept_key); - send(client_socket, buffer, n, 0); + if (n <= 0 || ws_send_all(client_socket, (unsigned char*)buffer, (size_t)n) < 0) + return -1; + return 0; +} + +/* --- Routes --- */ + +static void ws_register_route(const char* path, const chttpx_wsocket_callbacks_t* callbacks) +{ + if (!serv || !path || !callbacks) + return; + + if (serv->ws_routes_count == serv->ws_routes_capacity) + { + size_t new_cap = serv->ws_routes_capacity == 0 ? 4 : serv->ws_routes_capacity * 2; + chttpx_wsocket_route_entry_t* routes = + realloc(serv->ws_routes, sizeof(chttpx_wsocket_route_entry_t) * new_cap); + if (!routes) + return; + serv->ws_routes = routes; + serv->ws_routes_capacity = new_cap; + } + + serv->ws_routes[serv->ws_routes_count].path = strdup(path); + serv->ws_routes[serv->ws_routes_count].on_open = callbacks->on_open; + serv->ws_routes[serv->ws_routes_count].on_message = callbacks->on_message; + serv->ws_routes[serv->ws_routes_count].on_close = callbacks->on_close; + serv->ws_routes[serv->ws_routes_count].userdata = callbacks->userdata; + serv->ws_routes_count++; +} + +void cHTTPX_WSocketRegisterRoute(chttpx_router_t* r, const char* path, const chttpx_wsocket_callbacks_t* callbacks) +{ + if (!r || !path || !callbacks) + return; + + char fpath[CHTTPX_MAX_PATH]; + if (snprintf(fpath, sizeof(fpath), "%s%s", r->prefix ? r->prefix : "", path) >= (int)sizeof(fpath)) + return; + + ws_register_route(fpath, callbacks); +} + +static chttpx_wsocket_route_entry_t* find_ws_route(chttpx_request_t* req) +{ + if (!serv || !req || !req->path) + return NULL; + + for (size_t i = 0; i < serv->ws_routes_count; i++) + { + int count = 0; + if (cHTTPX_MatchPath(serv->ws_routes[i].path, req->path, req->params, &count)) + { + req->params_count = count; + return &serv->ws_routes[i]; + } + } + return NULL; +} + +static int is_websocket_upgrade(chttpx_request_t* req) +{ + if (!req || !req->method || strcmp(req->method, "GET") != 0) + return 0; + + const char* upgrade = cHTTPX_HeaderGet(req, "Upgrade"); + const char* connection = cHTTPX_HeaderGet(req, "Connection"); + const char* ws_key = cHTTPX_HeaderGet(req, "Sec-WebSocket-Key"); + const char* ws_version = cHTTPX_HeaderGet(req, "Sec-WebSocket-Version"); + + if (!upgrade || !connection || !ws_key || !ws_version) + return 0; + if (strcasecmp(upgrade, "websocket") != 0) + return 0; + if (!strstr(connection, "pgrade") && !strstr(connection, "pGRADE")) + return 0; + if (strcmp(ws_version, "13") != 0) + return 0; + return 1; +} + +/* --- Connection pool --- */ + +static void ws_connection_close(ws_connection_t* conn) +{ + if (!conn->public_ws.connected) + return; + + ws_send_frame(conn->public_ws.socket, CHTTPX_WSOCKET_OPCODE_CLOSE, NULL, 0, 1); + conn->public_ws.connected = 0; + + if (conn->on_close) + conn->on_close(&conn->public_ws, conn->route_userdata); + chttpx_close(conn->public_ws.socket); + conn->public_ws.socket = (chttpx_socket_t)-1; +} + +static void ws_remove_connection(size_t index) +{ + ws_connection_close(&ws_engine.items[index]); + ws_engine.items[index] = ws_engine.items[ws_engine.count - 1]; + ws_engine.count--; +} + +static int ws_add_connection(chttpx_socket_t fd, chttpx_wsocket_route_entry_t* route, chttpx_request_t* req) +{ + if (ws_engine.count == ws_engine.capacity) + { + size_t new_cap = ws_engine.capacity == 0 ? 64 : ws_engine.capacity * 2; + ws_connection_t* items = realloc(ws_engine.items, sizeof(ws_connection_t) * new_cap); + if (!items) + return -1; + ws_engine.items = items; + ws_engine.capacity = new_cap; + } + + size_t slot = ws_engine.count; + ws_connection_t* conn = &ws_engine.items[slot]; + memset(conn, 0, sizeof(*conn)); + conn->public_ws.socket = fd; + conn->public_ws.connected = 1; + if (req && req->path) + strncpy(conn->public_ws.path, req->path, sizeof(conn->public_ws.path) - 1); + if (req && req->params_count > 0) + { + conn->public_ws.params_count = req->params_count; + memcpy(conn->public_ws.params, req->params, sizeof(chttpx_param_t) * req->params_count); + } + conn->public_ws.userdata = route->userdata; + conn->on_open = route->on_open; + conn->on_message = route->on_message; + conn->on_close = route->on_close; + conn->route_userdata = route->userdata; + + ws_set_nonblocking(fd); + ws_engine.count++; + + return (int)slot; +} + +const char* cHTTPX_WSocketParam(chttpx_wsocket_t* ws, const char* name) +{ + if (!ws || !name || ws->params_count == 0) + return NULL; + + for (size_t i = 0; i < ws->params_count; i++) + { + if (strcmp(ws->params[i].name, name) == 0) + return ws->params[i].value; + } + return NULL; +} + +static int ws_broadcast_match(int (*match)(ws_connection_t* conn, const void* ctx), const void* ctx, const char* text) +{ + int sent = 0; + ws_lock(); + for (size_t i = 0; i < ws_engine.count; i++) + { + ws_connection_t* conn = &ws_engine.items[i]; + if (!conn->public_ws.connected) + continue; + if (!match(conn, ctx)) + continue; + if (cHTTPX_WSocketSend(&conn->public_ws, text) >= 0) + sent++; + } + ws_unlock(); + return sent; +} + +static int ws_match_path(ws_connection_t* conn, const void* ctx) +{ + const char* path = (const char*)ctx; + return path && strcmp(conn->public_ws.path, path) == 0; +} + +static int ws_match_peers(ws_connection_t* conn, const void* ctx) +{ + const chttpx_wsocket_t* ws = (const chttpx_wsocket_t*)ctx; + return ws && strcmp(conn->public_ws.path, ws->path) == 0; +} + +typedef struct +{ + const char* param_name; + const char* param_value; +} ws_room_match_t; + +static int ws_match_room(ws_connection_t* conn, const void* ctx) +{ + const ws_room_match_t* room = (const ws_room_match_t*)ctx; + if (!room || !room->param_name || !room->param_value) + return 0; + + for (size_t i = 0; i < conn->public_ws.params_count; i++) + { + if (strcmp(conn->public_ws.params[i].name, room->param_name) == 0 && + strcmp(conn->public_ws.params[i].value, room->param_value) == 0) + return 1; + } return 0; } -// static wsocket_read_frame(chttpx_socket_t client_fd, wsocket_frame_t* out) -// { -// unsigned char hdr[2]; +int cHTTPX_WSocketBroadcast(const char* path, const char* text) +{ + if (!path || !text) + return -1; + return ws_broadcast_match(ws_match_path, path, text); +} + +int cHTTPX_WSocketBroadcastPeers(chttpx_wsocket_t* ws, const char* text) +{ + if (!ws || !text) + return -1; + return ws_broadcast_match(ws_match_peers, ws, text); +} + +int cHTTPX_WSocketBroadcastRoom(const char* param_name, const char* param_value, const char* text) +{ + if (!param_name || !param_value || !text) + return -1; + + ws_room_match_t room = {param_name, param_value}; + return ws_broadcast_match(ws_match_room, &room, text); +} + +/* --- Frame parsing (incremental, non-blocking) --- */ + +static int ws_try_process_one_frame(ws_connection_t* conn) +{ + size_t off = conn->frame_offset; + size_t avail = conn->read_len - off; + + if (avail < 2) + return 0; + + unsigned char* b = conn->read_buf + off; + int opcode = b[0] & 0x0F; + int masked = (b[1] & 0x80) != 0; + uint64_t plen = b[1] & 0x7F; + size_t hsize = 2; + + if (plen == 126) + { + if (avail < 4) + return 0; + plen = ((uint64_t)b[2] << 8) | b[3]; + hsize = 4; + } + else if (plen == 127) + return -1; + + if (plen > CHTTPX_WSOCKET_MAX_PAYLOAD) + return -1; + + size_t total = hsize + (masked ? 4 : 0) + (size_t)plen; + if (avail < total) + return 0; + + unsigned char* payload = b + hsize + (masked ? 4 : 0); + unsigned char mask[4]; -// if (recv(client_fd, hdr, 2, MSG_WAITALL) <= 0) return -1; + if (masked) + { + memcpy(mask, b + hsize, 4); + for (uint64_t i = 0; i < plen; i++) + payload[i] ^= mask[i % 4]; + } -// int opcode = hdr[0] & 0x0F; -// int masked = hdr[1] & 0x80; -// int len = hdr[1] & 0x7F; -// } \ No newline at end of file + if (opcode == CHTTPX_WSOCKET_OPCODE_CLOSE) + { + conn->public_ws.connected = 0; + } + else if (opcode == CHTTPX_WSOCKET_OPCODE_PING) + { + ws_send_frame(conn->public_ws.socket, CHTTPX_WSOCKET_OPCODE_PONG, payload, (size_t)plen, 1); + } + else if (opcode == CHTTPX_WSOCKET_OPCODE_TEXT || opcode == CHTTPX_WSOCKET_OPCODE_BINARY) + { + if (conn->on_message) + conn->on_message(&conn->public_ws, payload, (size_t)plen, opcode, conn->route_userdata); + } + + conn->frame_offset += total; + if (conn->frame_offset >= conn->read_len) + { + conn->read_len = 0; + conn->frame_offset = 0; + } + else if (conn->frame_offset > 0) + { + memmove(conn->read_buf, conn->read_buf + conn->frame_offset, conn->read_len - conn->frame_offset); + conn->read_len -= conn->frame_offset; + conn->frame_offset = 0; + } + + return 1; +} + +static void ws_read_and_parse(ws_connection_t* conn) +{ + while (conn->public_ws.connected) + { + if (conn->read_len >= CHTTPX_WSOCKET_READ_BUF) + { + conn->public_ws.connected = 0; + return; + } + + ssize_t n = recv(conn->public_ws.socket, (char*)conn->read_buf + conn->read_len, + (int)(CHTTPX_WSOCKET_READ_BUF - conn->read_len), 0); + if (n < 0) + { +#ifdef CHTTPX_PLATFORM_WINDOWS + if (WSAGetLastError() == WSAEWOULDBLOCK) + return; +#else + if (errno == EWOULDBLOCK || errno == EAGAIN) + return; +#endif + conn->public_ws.connected = 0; + return; + } + if (n == 0) + { + conn->public_ws.connected = 0; + return; + } + + conn->read_len += (size_t)n; + + int processed; + do + { + processed = ws_try_process_one_frame(conn); + if (processed < 0) + { + conn->public_ws.connected = 0; + return; + } + } while (processed == 1 && conn->public_ws.connected); + return; + } +} + +/* --- Poll thread (select: works on Linux and Windows/MinGW) --- */ + +static void ws_poll_process_readable(ws_connection_t* conn) +{ + ws_read_and_parse(conn); +} + +static void* ws_poll_loop(void* arg) +{ + (void)arg; + + while (ws_engine.engine_running && !ws_engine.shutdown_requested) + { + ws_lock(); + size_t n = ws_engine.count; + + chttpx_socket_t* fds = NULL; + if (n > 0) + { + fds = malloc(sizeof(chttpx_socket_t) * n); + if (fds) + { + for (size_t i = 0; i < n; i++) + fds[i] = ws_engine.items[i].public_ws.socket; + } + else + n = 0; + } + ws_unlock(); + + if (n == 0) + { + free(fds); +#if defined(_WIN32) || defined(_WIN64) + Sleep(10); +#else + usleep(10000); +#endif + continue; + } + + fd_set readfds; + FD_ZERO(&readfds); + chttpx_socket_t maxfd = 0; + for (size_t i = 0; i < n; i++) + { + FD_SET(fds[i], &readfds); + if (fds[i] > maxfd) + maxfd = fds[i]; + } + + struct timeval tv = {.tv_sec = 0, .tv_usec = 50000}; + int ready = select((int)(maxfd + 1), &readfds, NULL, NULL, &tv); + + if (ready <= 0) + { + free(fds); + continue; + } + + ws_lock(); + for (size_t i = 0; i < ws_engine.count;) + { + ws_connection_t* conn = &ws_engine.items[i]; + if (!conn->public_ws.connected) + { + ws_remove_connection(i); + continue; + } + if (FD_ISSET(conn->public_ws.socket, &readfds)) + { + ws_unlock(); + ws_poll_process_readable(conn); + ws_lock(); + if (i >= ws_engine.count) + break; + conn = &ws_engine.items[i]; + } + if (!conn->public_ws.connected) + { + ws_remove_connection(i); + continue; + } + i++; + } + ws_unlock(); + free(fds); + } + + return NULL; +} + +static void ws_engine_start(void) +{ + if (ws_engine.engine_running) + return; + +#if defined(_WIN32) || defined(_WIN64) + InitializeCriticalSection(&ws_engine.lock); +#else + pthread_mutex_init(&ws_engine.lock, NULL); +#endif + + ws_engine.engine_running = 1; + ws_engine.shutdown_requested = 0; + _thread_create(&ws_engine.poll_thread, ws_poll_loop, NULL); +} + +void cHTTPX_WSocketShutdown(void) +{ + if (!ws_engine.engine_running) + return; + + ws_engine.shutdown_requested = 1; + _thread_join(ws_engine.poll_thread); + + ws_lock(); + while (ws_engine.count > 0) + ws_remove_connection(0); + free(ws_engine.items); + ws_engine.items = NULL; + ws_engine.count = 0; + ws_engine.capacity = 0; + ws_unlock(); + +#if defined(_WIN32) || defined(_WIN64) + DeleteCriticalSection(&ws_engine.lock); +#else + pthread_mutex_destroy(&ws_engine.lock); +#endif + + ws_engine.engine_running = 0; +} + +int cHTTPX_WSocketTryHandle(chttpx_request_t* req) +{ + if (!is_websocket_upgrade(req)) + return 0; + + chttpx_wsocket_route_entry_t* route = find_ws_route(req); + if (!route) + return 0; + + const char* ws_key = cHTTPX_HeaderGet(req, "Sec-WebSocket-Key"); + if (ws_do_upgrade(req->client_fd, ws_key) != 0) + return -1; + + ws_engine_start(); + + ws_lock(); + int slot = ws_add_connection(req->client_fd, route, req); + ws_unlock(); + if (slot < 0) + { + chttpx_close(req->client_fd); + return -1; + } + + ws_connection_t* conn = &ws_engine.items[(size_t)slot]; + if (conn->on_open) + conn->on_open(&conn->public_ws, conn->route_userdata); + + return 1; +} diff --git a/tests/test_framework.h b/tests/test_framework.h new file mode 100644 index 0000000..89bf5ab --- /dev/null +++ b/tests/test_framework.h @@ -0,0 +1,64 @@ +#ifndef TEST_FRAMEWORK_H +#define TEST_FRAMEWORK_H + +#include +#include + +extern int g_tests_run; +extern int g_tests_failed; + +#define TEST(name) static void name(void) + +#define RUN_TEST(name) \ + do \ + { \ + int _failed_before = g_tests_failed; \ + printf(" %s ... ", #name); \ + fflush(stdout); \ + g_tests_run++; \ + name(); \ + if (g_tests_failed == _failed_before) \ + printf("ok\n"); \ + } while (0) + +#define ASSERT(cond) \ + do \ + { \ + if (!(cond)) \ + { \ + printf("FAIL\n assertion: %s (%s:%d)\n", #cond, \ + __FILE__, __LINE__); \ + g_tests_failed++; \ + return; \ + } \ + } while (0) + +#define ASSERT_STREQ(expected, actual) \ + do \ + { \ + const char* _exp = (expected); \ + const char* _act = (actual); \ + if (!_exp || !_act || strcmp(_exp, _act) != 0) \ + { \ + printf("FAIL\n expected '%s', got '%s' (%s:%d)\n", _exp, _act, \ + __FILE__, __LINE__); \ + g_tests_failed++; \ + return; \ + } \ + } while (0) + +#define ASSERT_EQ(expected, actual) \ + do \ + { \ + long long _exp = (long long)(expected); \ + long long _act = (long long)(actual); \ + if (_exp != _act) \ + { \ + printf("FAIL\n expected %lld, got %lld (%s:%d)\n", _exp, _act, \ + __FILE__, __LINE__); \ + g_tests_failed++; \ + return; \ + } \ + } while (0) + +#endif diff --git a/tests/test_params.c b/tests/test_params.c new file mode 100644 index 0000000..a8ba239 --- /dev/null +++ b/tests/test_params.c @@ -0,0 +1,60 @@ +#include "test_framework.h" + +#include "libchttpx.h" + +TEST(test_match_static_path) +{ + chttpx_param_t params[MAX_PARAMS] = {0}; + int count = -1; + + ASSERT(cHTTPX_MatchPath("/api/v1/", "/api/v1/", params, &count)); + ASSERT_EQ(0, count); +} + +TEST(test_match_single_param) +{ + chttpx_param_t params[MAX_PARAMS] = {0}; + int count = 0; + + ASSERT(cHTTPX_MatchPath("/users/{uuid}", "/users/abc-123", params, &count)); + ASSERT_EQ(1, count); + ASSERT_STREQ("uuid", params[0].name); + ASSERT_STREQ("abc-123", params[0].value); +} + +TEST(test_match_ws_room_path) +{ + chttpx_param_t params[MAX_PARAMS] = {0}; + int count = 0; + + ASSERT(cHTTPX_MatchPath("/api/v1/ws/chat/{room_id}", "/api/v1/ws/chat/lobby", params, &count)); + ASSERT_EQ(1, count); + ASSERT_STREQ("room_id", params[0].name); + ASSERT_STREQ("lobby", params[0].value); +} + +TEST(test_match_rejects_extra_segments) +{ + chttpx_param_t params[MAX_PARAMS] = {0}; + int count = 0; + + ASSERT(!cHTTPX_MatchPath("/users/{uuid}", "/users/abc/extra", params, &count)); +} + +TEST(test_match_rejects_missing_segments) +{ + chttpx_param_t params[MAX_PARAMS] = {0}; + int count = 0; + + ASSERT(!cHTTPX_MatchPath("/users/{uuid}/profile", "/users/abc", params, &count)); +} + +void run_params_tests(void) +{ + printf("params\n"); + RUN_TEST(test_match_static_path); + RUN_TEST(test_match_single_param); + RUN_TEST(test_match_ws_room_path); + RUN_TEST(test_match_rejects_extra_segments); + RUN_TEST(test_match_rejects_missing_segments); +} diff --git a/tests/test_response.c b/tests/test_response.c new file mode 100644 index 0000000..628ab2c --- /dev/null +++ b/tests/test_response.c @@ -0,0 +1,48 @@ +#include "test_framework.h" + +#include "libchttpx.h" + +#include +#include + +TEST(test_res_json_body) +{ + chttpx_response_t res = cHTTPX_ResJson(cHTTPX_StatusOK, "{\"msg\":\"hi\"}"); + + ASSERT_EQ(cHTTPX_StatusOK, res.status); + ASSERT(res.body != NULL); + ASSERT_STREQ("application/json", res.content_type); + ASSERT_STREQ("{\"msg\":\"hi\"}", (const char*)res.body); + ASSERT_EQ(12, (long long)res.body_size); + + free(res.body); +} + +TEST(test_res_html_body) +{ + chttpx_response_t res = cHTTPX_ResHtml(cHTTPX_StatusOK, "

%s

", "test"); + + ASSERT_EQ(cHTTPX_StatusOK, res.status); + ASSERT(res.body != NULL); + ASSERT_STREQ("text/html", res.content_type); + ASSERT_STREQ("

test

", (const char*)res.body); + + free(res.body); +} + +TEST(test_res_json_not_found) +{ + chttpx_response_t res = cHTTPX_ResJson(cHTTPX_StatusNotFound, "{\"error\":\"missing\"}"); + + ASSERT_EQ(cHTTPX_StatusNotFound, res.status); + ASSERT(res.body != NULL); + free(res.body); +} + +void run_response_tests(void) +{ + printf("response\n"); + RUN_TEST(test_res_json_body); + RUN_TEST(test_res_html_body); + RUN_TEST(test_res_json_not_found); +} diff --git a/tests/test_runner.c b/tests/test_runner.c new file mode 100644 index 0000000..10701e1 --- /dev/null +++ b/tests/test_runner.c @@ -0,0 +1,21 @@ +#include + +int g_tests_run = 0; +int g_tests_failed = 0; + +void run_params_tests(void); +void run_response_tests(void); +void run_server_tests(void); +void run_websocket_tests(void); + +int main(void) +{ + run_params_tests(); + run_response_tests(); + run_server_tests(); + run_websocket_tests(); + + printf("\n%d tests, %d failed\n", g_tests_run, g_tests_failed); + + return g_tests_failed == 0 ? 0 : 1; +} diff --git a/tests/test_server.c b/tests/test_server.c new file mode 100644 index 0000000..ff490f4 --- /dev/null +++ b/tests/test_server.c @@ -0,0 +1,64 @@ +#include "test_framework.h" + +#include "libchttpx.h" + +#include + +static void ping_handler(chttpx_request_t* req, chttpx_response_t* res) +{ + (void)req; + *res = cHTTPX_ResJson(cHTTPX_StatusOK, "{\"pong\":true}"); +} + +TEST(test_init_and_shutdown) +{ + chttpx_serv_t serv = {0}; + + ASSERT_EQ(0, cHTTPX_Init(&serv, 18080, NULL)); + ASSERT_EQ(18080, serv.port); + ASSERT(serv.routes == NULL); + ASSERT_EQ(0, serv.routes_count); + + cHTTPX_Shutdown(); +} + +TEST(test_register_http_routes) +{ + chttpx_serv_t serv = {0}; + + ASSERT_EQ(0, cHTTPX_Init(&serv, 18081, NULL)); + + chttpx_router_t api = cHTTPX_RoutePathPrefix("/api/v1"); + cHTTPX_RegisterRoute(&api, "GET", "/ping", ping_handler); + cHTTPX_RegisterRoute(&api, "POST", "/users", ping_handler); + + ASSERT_EQ(2, (long long)serv.routes_count); + ASSERT_STREQ("GET", serv.routes[0].method); + ASSERT_STREQ("/api/v1/ping", serv.routes[0].path); + ASSERT_STREQ("POST", serv.routes[1].method); + ASSERT_STREQ("/api/v1/users", serv.routes[1].path); + + cHTTPX_Shutdown(); +} + +TEST(test_middleware_registration) +{ + chttpx_serv_t serv = {0}; + + ASSERT_EQ(0, cHTTPX_Init(&serv, 18082, NULL)); + + cHTTPX_MiddlewareRecovery(); + cHTTPX_MiddlewareRateLimiter(5, 1); + + ASSERT(serv.middleware.middleware_count >= 2); + + cHTTPX_Shutdown(); +} + +void run_server_tests(void) +{ + printf("server\n"); + RUN_TEST(test_init_and_shutdown); + RUN_TEST(test_register_http_routes); + RUN_TEST(test_middleware_registration); +} diff --git a/tests/test_websocket.c b/tests/test_websocket.c new file mode 100644 index 0000000..bf56452 --- /dev/null +++ b/tests/test_websocket.c @@ -0,0 +1,72 @@ +#include "test_framework.h" + +#include "libchttpx.h" + +static void ws_open(chttpx_wsocket_t* ws, void* userdata) +{ + (void)ws; + (void)userdata; +} + +static void ws_message(chttpx_wsocket_t* ws, const unsigned char* data, size_t len, int opcode, void* userdata) +{ + (void)ws; + (void)data; + (void)len; + (void)opcode; + (void)userdata; +} + +static void ws_close(chttpx_wsocket_t* ws, void* userdata) +{ + (void)ws; + (void)userdata; +} + +TEST(test_register_websocket_route) +{ + chttpx_serv_t serv = {0}; + static chttpx_wsocket_callbacks_t callbacks = { + .on_open = ws_open, + .on_message = ws_message, + .on_close = ws_close, + }; + + ASSERT_EQ(0, cHTTPX_Init(&serv, 18083, NULL)); + + chttpx_router_t api = cHTTPX_RoutePathPrefix("/api/v1"); + cHTTPX_WSocketRegisterRoute(&api, "/ws/chat/{room_id}", &callbacks); + + ASSERT_EQ(1, (long long)serv.ws_routes_count); + ASSERT(serv.ws_routes[0].path != NULL); + ASSERT_STREQ("/api/v1/ws/chat/{room_id}", serv.ws_routes[0].path); + ASSERT(serv.ws_routes[0].on_open == ws_open); + ASSERT(serv.ws_routes[0].on_message == ws_message); + ASSERT(serv.ws_routes[0].on_close == ws_close); + + cHTTPX_Shutdown(); +} + +TEST(test_websocket_shutdown_without_connections) +{ + chttpx_serv_t serv = {0}; + static chttpx_wsocket_callbacks_t callbacks = { + .on_open = ws_open, + .on_message = ws_message, + .on_close = ws_close, + }; + + ASSERT_EQ(0, cHTTPX_Init(&serv, 18084, NULL)); + + chttpx_router_t api = cHTTPX_RoutePathPrefix("/api/v1"); + cHTTPX_WSocketRegisterRoute(&api, "/ws/chat/{room_id}", &callbacks); + + cHTTPX_Shutdown(); +} + +void run_websocket_tests(void) +{ + printf("websocket\n"); + RUN_TEST(test_register_websocket_route); + RUN_TEST(test_websocket_shutdown_without_connections); +}