From 8971fc069a1c1a9e8c3f6fa1f83e64a50b2876c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Helleu?= Date: Sat, 6 Jan 2024 10:42:22 +0100 Subject: [PATCH] relay: add "api" protocol (HTTP REST API) (issue #2066) --- .github/workflows/ci.yml | 39 +- CMakeLists.txt | 7 + ChangeLog.adoc | 1 + debian-devel/control | 3 +- debian-stable/control | 3 +- doc/de/weechat_user.de.adoc | 8 + doc/en/weechat_dev.en.adoc | 8 +- doc/en/weechat_user.en.adoc | 6 + doc/fr/weechat_dev.fr.adoc | 8 +- doc/fr/weechat_user.fr.adoc | 6 + doc/it/weechat_user.it.adoc | 8 + doc/ja/weechat_dev.ja.adoc | 15 +- doc/ja/weechat_user.ja.adoc | 8 + doc/pl/weechat_user.pl.adoc | 8 + doc/sr/weechat_dev.sr.adoc | 15 +- doc/sr/weechat_user.sr.adoc | 8 + po/srcfiles.cmake | 8 + src/core/CMakeLists.txt | 4 + src/core/wee-debug.c | 13 + src/plugins/relay/CMakeLists.txt | 20 +- src/plugins/relay/api/relay-api-msg.c | 601 ++++++++ src/plugins/relay/api/relay-api-msg.h | 51 + src/plugins/relay/api/relay-api-protocol.c | 878 +++++++++++ src/plugins/relay/api/relay-api-protocol.h | 67 + src/plugins/relay/api/relay-api.c | 311 ++++ src/plugins/relay/api/relay-api.h | 83 ++ src/plugins/relay/relay-client.c | 541 +++---- src/plugins/relay/relay-client.h | 8 +- src/plugins/relay/relay-config.c | 130 +- src/plugins/relay/relay-config.h | 2 + src/plugins/relay/relay-http.c | 1284 +++++++++++++++++ src/plugins/relay/relay-http.h | 92 ++ src/plugins/relay/relay-server.c | 17 +- src/plugins/relay/relay-websocket.c | 125 +- src/plugins/relay/relay-websocket.h | 7 +- src/plugins/relay/relay.c | 2 +- src/plugins/relay/relay.h | 1 + tests/CMakeLists.txt | 1 + tests/unit/plugins/relay/test-relay-http.cpp | 742 ++++++++++ .../patches/weechat_ubuntu_bionic.patch | 48 +- weechat.cygport.in | 5 +- 41 files changed, 4811 insertions(+), 381 deletions(-) create mode 100644 src/plugins/relay/api/relay-api-msg.c create mode 100644 src/plugins/relay/api/relay-api-msg.h create mode 100644 src/plugins/relay/api/relay-api-protocol.c create mode 100644 src/plugins/relay/api/relay-api-protocol.h create mode 100644 src/plugins/relay/api/relay-api.c create mode 100644 src/plugins/relay/api/relay-api.h create mode 100644 src/plugins/relay/relay-http.c create mode 100644 src/plugins/relay/relay-http.h create mode 100644 tests/unit/plugins/relay/test-relay-http.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b93d5ea29..7cbf586dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,41 @@ on: - pull_request env: - WEECHAT_DEPENDENCIES: devscripts equivs python3-pip cmake ninja-build lcov pkg-config libncurses-dev gem2deb libperl-dev python3-dev libaspell-dev liblua5.3-dev tcl8.6-dev guile-3.0-dev libv8-dev libcurl4-gnutls-dev libgcrypt20-dev libgnutls28-dev libzstd-dev zlib1g-dev curl libcpputest-dev php-dev libphp-embed libargon2-dev libsodium-dev flake8 pylint python3-bandit asciidoctor ruby-pygments.rb shellcheck + WEECHAT_DEPENDENCIES: >- + asciidoctor + cmake + curl + devscripts + equivs + flake8 + gem2deb + guile-3.0-dev + lcov + libargon2-dev + libaspell-dev + libcjson-dev + libcpputest-dev + libcurl4-gnutls-dev + libgcrypt20-dev + libgnutls28-dev + liblua5.3-dev + libncurses-dev + libperl-dev + libphp-embed + libsodium-dev + libv8-dev + libzstd-dev + ninja-build + php-dev + pkg-config + pylint + python3-bandit + python3-dev + python3-pip + ruby-pygments.rb + shellcheck + tcl8.6-dev + zlib1g-dev jobs: @@ -20,6 +54,7 @@ jobs: - { name: "gcc_ninja", cc: "gcc", cxx: "g++", buildargs: "-G Ninja" } - { name: "gcc_no_nls", cc: "gcc", cxx: "g++", buildargs: "-DENABLE_NLS=OFF -DENABLE_DOC=OFF" } - { name: "gcc_no_zstd", cc: "gcc", cxx: "g++", buildargs: "-DENABLE_ZSTD=OFF -DENABLE_DOC=OFF" } + - { name: "gcc_no_cjson", cc: "gcc", cxx: "g++", buildargs: "-DENABLE_CJSON=OFF -DENABLE_DOC=OFF" } - { name: "gcc_coverage", cc: "gcc", cxx: "g++", buildargs: "-DENABLE_CODE_COVERAGE=ON" } - { name: "clang", cc: "clang", cxx: "clang++", buildargs: "" } @@ -115,7 +150,7 @@ jobs: /usr/local/bin/python3.11 \ /usr/local/bin/python3.11-config \ ; - brew install asciidoctor guile lua pkg-config ruby + brew install asciidoctor cjson guile lua pkg-config ruby - uses: actions/checkout@v2 diff --git a/CMakeLists.txt b/CMakeLists.txt index 229395c51..1dd3ae1ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ option(ENABLE_HEADLESS "Compile the headless binary" ON) option(ENABLE_NLS "Enable Native Language Support" ON) option(ENABLE_LARGEFILE "Enable Large File Support" ON) option(ENABLE_ZSTD "Enable Zstandard compression" ON) +option(ENABLE_CJSON "Enable cJSON support" ON) option(ENABLE_ALIAS "Enable Alias plugin" ON) option(ENABLE_BUFLIST "Enable Buflist plugin" ON) option(ENABLE_CHARSET "Enable Charset plugin" ON) @@ -222,6 +223,12 @@ if(ENABLE_ZSTD) add_definitions(-DHAVE_ZSTD) endif() +# Check for cJSON +if(ENABLE_CJSON) + pkg_check_modules(LIBCJSON REQUIRED libcjson) + add_definitions(-DHAVE_CJSON) +endif() + # Check for iconv find_package(Iconv) if(ICONV_FOUND) diff --git a/ChangeLog.adoc b/ChangeLog.adoc index 4cb0a866b..593167886 100644 --- a/ChangeLog.adoc +++ b/ChangeLog.adoc @@ -18,6 +18,7 @@ New features:: * core: allow case insensitive search of partial buffer name with `(?i)name` in command `/buffer` * core: use function util_strftimeval in evaluation of expression `date:xxx` * api: add support of specifier `%!` for timestamp in function util_strftimeval + * relay: add "api" protocol (HTTP REST API), add option relay.look.display_clients, change option type relay.look.auto_open_buffer to string (issue #2066) Bug fixes:: diff --git a/debian-devel/control b/debian-devel/control index ec3a7b157..a5d24b6a8 100644 --- a/debian-devel/control +++ b/debian-devel/control @@ -21,7 +21,8 @@ Build-Depends: libgcrypt20-dev, libgnutls28-dev, libzstd-dev, - zlib1g-dev + zlib1g-dev, + libcjson-dev Standards-Version: 4.6.2 Homepage: https://weechat.org/ Vcs-Git: https://salsa.debian.org/kolter/weechat.git diff --git a/debian-stable/control b/debian-stable/control index ed5e3d461..a75e6fee5 100644 --- a/debian-stable/control +++ b/debian-stable/control @@ -21,7 +21,8 @@ Build-Depends: libgcrypt20-dev, libgnutls28-dev, libzstd-dev, - zlib1g-dev + zlib1g-dev, + libcjson-dev Standards-Version: 4.6.2 Homepage: https://weechat.org/ Vcs-Git: https://salsa.debian.org/kolter/weechat.git diff --git a/doc/de/weechat_user.de.adoc b/doc/de/weechat_user.de.adoc index a08738cd6..9edb529a3 100644 --- a/doc/de/weechat_user.de.adoc +++ b/doc/de/weechat_user.de.adoc @@ -159,6 +159,10 @@ WeeChat optional sind: | ca-certificates | | Zertifikate für TLS Verbindungen. +// TRANSLATION MISSING +| libcjson-dev | +| Relay plugin: protocol "api" (HTTP REST API). + | libzstd-dev | ≥ 0.8.1 | Logger-Erweiterung: Kompression von rotierenden Protokolldateien (zstandard). + Relay-Erweiterung: Kompression von Nachrichten (WeeChat -> client) with https://facebook.github.io/zstd/[Zstandard ^↗^^] (weechat protocol). @@ -279,6 +283,10 @@ Liste der verfügbaren Optionen: | ENABLE_CHARSET | `ON`, `OFF` | ON | kompiliert <>. +// TRANSLATION MISSING +| ENABLE_CJSON | `ON`, `OFF` | ON +| Support of JSON with https://github.com/DaveGamble/cJSON[cJSON ^↗^^] library. + | ENABLE_MAN | `ON`, `OFF` | OFF | erstellt die man page. diff --git a/doc/en/weechat_dev.en.adoc b/doc/en/weechat_dev.en.adoc index b86822afd..24833ee65 100644 --- a/doc/en/weechat_dev.en.adoc +++ b/doc/en/weechat_dev.en.adoc @@ -331,15 +331,20 @@ WeeChat "core" is located in following directories: |       relay-command.c | Relay commands. |       relay-completion.c | Relay completions. |       relay-config.c | Relay config options (file relay.conf). +|       relay-http.c | HTTP functions. |       relay-info.c | Relay info/infolists/hdata. |       relay-network.c | Network functions for relay. |       relay-raw.c | Relay raw buffer. |       relay-server.c | Relay server. |       relay-upgrade.c | Save/restore of relay data when upgrading WeeChat. |       relay-websocket.c | WebSocket server functions (RFC 6455). +|       api/ | Relay for remote interfaces (using HTTP REST API). +|          relay-api.c | Main API functions for HTTP REST API. +|          relay-api-msg.c | Send JSON messages to clients. +|          relay-api-protocol.c | HTTP REST API protocol. |       irc/ | IRC proxy. |          relay-irc.c | Main IRC proxy functions. -|       weechat/ | Relay for remote interfaces. +|       weechat/ | Relay for remote interfaces (using "weechat" binary protocol). |          relay-weechat.c | Relay for remote interfaces (main functions). |          relay-weechat-msg.c | Send binary messages to clients. |          relay-weechat-nicklist.c | Nicklist functions. @@ -469,6 +474,7 @@ WeeChat "core" is located in following directories: |             test-typing-status.cpp | Tests: typing status. |          relay/ | Root of unit tests for Relay plugin. |             test-relay-auth.cpp | Tests: clients authentication. +|             test-relay-http.cpp | Tests: HTTP functions for Relay plugin. |             irc/ | Root of unit tests for Relay "irc" protocol. |                test-relay-irc.cpp | Tests: Relay "irc" protocol. |          xfer/ | Root of unit tests for Xfer plugin. diff --git a/doc/en/weechat_user.en.adoc b/doc/en/weechat_user.en.adoc index 63aa02fb5..b3d13aac0 100644 --- a/doc/en/weechat_user.en.adoc +++ b/doc/en/weechat_user.en.adoc @@ -154,6 +154,9 @@ WeeChat: | ca-certificates | | Certificates for TLS connections. +| libcjson-dev | +| Relay plugin: protocol "api" (HTTP REST API). + | libzstd-dev | ≥ 0.8.1 | Logger plugin: compression of rotated log files (zstandard). + Relay plugin: compression of messages (WeeChat -> client) with https://facebook.github.io/zstd/[Zstandard ^↗^^] (weechat protocol). @@ -273,6 +276,9 @@ List of available options: | ENABLE_CHARSET | `ON`, `OFF` | ON | Compile <>. +| ENABLE_CJSON | `ON`, `OFF` | ON +| Support of JSON with https://github.com/DaveGamble/cJSON[cJSON ^↗^^] library. + | ENABLE_MAN | `ON`, `OFF` | OFF | Build man page. diff --git a/doc/fr/weechat_dev.fr.adoc b/doc/fr/weechat_dev.fr.adoc index df83e70e2..08ccbc83d 100644 --- a/doc/fr/weechat_dev.fr.adoc +++ b/doc/fr/weechat_dev.fr.adoc @@ -333,15 +333,20 @@ Le cœur de WeeChat est situé dans les répertoires suivants : |       relay-command.c | Commandes de Relay. |       relay-completion.c | Complétions de Relay. |       relay-config.c | Options de configuration pour Relay (fichier relay.conf). +|       relay-http.c | Fonctions HTTP. |       relay-info.c | Info/infolists/hdata pour Relay. |       relay-network.c | Fonctions de réseau pour Relay. |       relay-raw.c | Tampon des données brutes de Relay. |       relay-server.c | Serveur Relay. |       relay-upgrade.c | Sauvegarde/restauration des données Relay lors de la mise à jour de WeeChat. |       relay-websocket.c | Fonctions pour le serveur WebSocket (RFC 6455). +|       api/ | Relai pour les interfaces distantes (en utilisant une API REST HTTP). +|          relay-api.c | Fonctions principales pour l'API REST HTTP. +|          relay-api-msg.c | Envoi de messages JSON aux clients. +|          relay-api-protocol.c | Protocole HTTP REST API. |       irc/ | Proxy IRC. |          relay-irc.c | Fonctions principales pour le proxy IRC. -|       weechat/ | Relai pour les interfaces distantes. +|       weechat/ | Relai pour les interfaces distantes (en utilisant le protocole binaire "weechat"). |          relay-weechat.c | Relai pour les interfaces distantes (fonctions principales). |          relay-weechat-msg.c | Envoi de messages binaires aux clients. |          relay-weechat-nicklist.c | Fonctions pour la liste de pseudos. @@ -471,6 +476,7 @@ Le cœur de WeeChat est situé dans les répertoires suivants : |             test-typing-status.cpp | Tests : statut d'écriture. |          relay/ | Racine des tests unitaires pour l'extension Relay. |             test-relay-auth.cpp | Tests : authentification des clients. +|             test-relay-http.cpp | Tests : fonctions HTTP pour l'extension Relay. |             irc/ | Racine des tests unitaires pour le protocole relay "irc". |                test-relay-irc.cpp | Tests : Protocole relay "irc". |          xfer/ | Racine des tests unitaires pour l'extension Xfer. diff --git a/doc/fr/weechat_user.fr.adoc b/doc/fr/weechat_user.fr.adoc index 70b83427b..43894886e 100644 --- a/doc/fr/weechat_user.fr.adoc +++ b/doc/fr/weechat_user.fr.adoc @@ -153,6 +153,9 @@ Le tableau suivant liste les paquets optionnels pour compiler WeeChat : | ca-certificates | | Certificats pour les connexions TLS. +| libcjson-dev | +| Extension Relay : protocole "api" (API REST HTTP). + | libzstd-dev | ≥ 0.8.1 | Extension Logger : compression des fichiers de log qui tournent (zstandard). + Extension Relay : compression des messages (WeeChat -> client) avec https://facebook.github.io/zstd/[Zstandard ^↗^^] (protocole weechat). @@ -272,6 +275,9 @@ Liste des options disponibles : | ENABLE_CHARSET | `ON`, `OFF` | ON | Compiler <>. +| ENABLE_CJSON | `ON`, `OFF` | ON +| Support du JSON avec la bibliothèque https://github.com/DaveGamble/cJSON[cJSON ^↗^^]. + | ENABLE_MAN | `ON`, `OFF` | OFF | Construire la page man. diff --git a/doc/it/weechat_user.it.adoc b/doc/it/weechat_user.it.adoc index ea06ec017..d768c5c72 100644 --- a/doc/it/weechat_user.it.adoc +++ b/doc/it/weechat_user.it.adoc @@ -188,6 +188,10 @@ WeeChat: | ca-certificates | | Certificati per le connessioni TLS. +// TRANSLATION MISSING +| libcjson-dev | +| Relay plugin: protocol "api" (HTTP REST API). + // TRANSLATION MISSING | libzstd-dev | ≥ 0.8.1 | Logger plugin: compression of rotated log files (zstandard). + @@ -320,6 +324,10 @@ List of available options: | ENABLE_CHARSET | `ON`, `OFF` | ON | Compile <>. +// TRANSLATION MISSING +| ENABLE_CJSON | `ON`, `OFF` | ON +| Support of JSON with https://github.com/DaveGamble/cJSON[cJSON ^↗^^] library. + | ENABLE_MAN | `ON`, `OFF` | OFF | Build man page. diff --git a/doc/ja/weechat_dev.ja.adoc b/doc/ja/weechat_dev.ja.adoc index 0983e11c0..e95e72b57 100644 --- a/doc/ja/weechat_dev.ja.adoc +++ b/doc/ja/weechat_dev.ja.adoc @@ -352,15 +352,26 @@ WeeChat "core" は以下のディレクトリに配置されています: |       relay-command.c | relay コマンド |       relay-completion.c | relay 補完 |       relay-config.c | relay 設定オプション (relay.conf ファイル) +// TRANSLATION MISSING +|       relay-http.c | HTTP functions. |       relay-info.c | relay の情報/インフォリスト/hdata |       relay-network.c | relay 用のネットワーク関数 |       relay-raw.c | relay 生バッファ |       relay-server.c | relay サーバ |       relay-upgrade.c | WeeChat をアップグレードする際にデータを保存/回復 |       relay-websocket.c | リレー用の websocket サーバ関数 (RFC 6455) +// TRANSLATION MISSING +|       api/ | Relay for remote interfaces (using HTTP REST API). +// TRANSLATION MISSING +|          relay-api.c | Main API functions for HTTP REST API. +// TRANSLATION MISSING +|          relay-api-msg.c | Send JSON messages to clients. +// TRANSLATION MISSING +|          relay-api-protocol.c | HTTP REST API protocol. |       irc/ | IRC プロキシ |          relay-irc.c | IRC プロキシの主要関数 -|       weechat/ | リモートインターフェースへの中継 +// TRANSLATION MISSING +|       weechat/ | Relay for remote interfaces (using "weechat" binary protocol). |          relay-weechat.c | リモートインターフェースへの中継 (主要関数) |          relay-weechat-msg.c | クライアントにバイナリメッセージを送信 |          relay-weechat-nicklist.c | ニックネームリスト関数 @@ -543,6 +554,8 @@ WeeChat "core" は以下のディレクトリに配置されています: // TRANSLATION MISSING |             test-relay-auth.cpp | Tests: clients authentication. // TRANSLATION MISSING +|             test-relay-http.cpp | Tests: HTTP functions for Relay plugin. +// TRANSLATION MISSING |             irc/ | Root of unit tests for Relay "irc" protocol. // TRANSLATION MISSING |                test-relay-irc.cpp | Tests: Relay "irc" protocol. diff --git a/doc/ja/weechat_user.ja.adoc b/doc/ja/weechat_user.ja.adoc index bda337731..0c31ecaa1 100644 --- a/doc/ja/weechat_user.ja.adoc +++ b/doc/ja/weechat_user.ja.adoc @@ -173,6 +173,10 @@ WeeChat: | ca-certificates | | TLS 接続に必要な証明書、relay プラグインで TLS サポート +// TRANSLATION MISSING +| libcjson-dev | +| Relay plugin: protocol "api" (HTTP REST API). + // TRANSLATION MISSING | libzstd-dev | ≥ 0.8.1 | Logger plugin: compression of rotated log files (zstandard). + @@ -298,6 +302,10 @@ List of available options: | ENABLE_CHARSET | `ON`, `OFF` | ON | <>のコンパイル。 +// TRANSLATION MISSING +| ENABLE_CJSON | `ON`, `OFF` | ON +| Support of JSON with https://github.com/DaveGamble/cJSON[cJSON ^↗^^] library. + | ENABLE_MAN | `ON`, `OFF` | OFF | man ページのコンパイル。 diff --git a/doc/pl/weechat_user.pl.adoc b/doc/pl/weechat_user.pl.adoc index 2ac9f491d..f900f455e 100644 --- a/doc/pl/weechat_user.pl.adoc +++ b/doc/pl/weechat_user.pl.adoc @@ -158,6 +158,10 @@ WeeChat: | ca-certificates | | Certyfikaty dla połączeń TLS. +// TRANSLATION MISSING +| libcjson-dev | +| Relay plugin: protocol "api" (HTTP REST API). + | libzstd-dev | ≥ 0.8.1 | Wtyczka logger: kompresja rotowanych plików z logami (zstandard). + Wtyczka Relay: kompresja wiadomości (WeeChat -> klient) za pomocą https://facebook.github.io/zstd/[Zstandard ^↗^^] (protokół weechat). @@ -277,6 +281,10 @@ Lista dostępnych opcji: | ENABLE_CHARSET | `ON`, `OFF` | ON | Kompilacja <>. +// TRANSLATION MISSING +| ENABLE_CJSON | `ON`, `OFF` | ON +| Support of JSON with https://github.com/DaveGamble/cJSON[cJSON ^↗^^] library. + | ENABLE_MAN | `ON`, `OFF` | OFF | Generowanie strony manuala. diff --git a/doc/sr/weechat_dev.sr.adoc b/doc/sr/weechat_dev.sr.adoc index fd3995e09..b6924ecec 100644 --- a/doc/sr/weechat_dev.sr.adoc +++ b/doc/sr/weechat_dev.sr.adoc @@ -333,15 +333,26 @@ WeeChat „језгро” се налази у следећим директо |       relay-command.c | Релеј команде. |       relay-completion.c | Релеј довршавања. |       relay-config.c | Релеј опције конфиг (фајл relay.conf). +// TRANSLATION MISSING +|       relay-http.c | HTTP functions. |       relay-info.c | Релеј info/infolists/hdata. |       relay-network.c | Мрежне функције за релеј. |       relay-raw.c | Релеј сирови бафер. |       relay-server.c | Релеј сервер. |       relay-upgrade.c | Save/restore of relay data when upgrading WeeChat. |       relay-websocket.c | WebSocket сервер функције (RFC 6455). +// TRANSLATION MISSING +|       api/ | Relay for remote interfaces (using HTTP REST API). +// TRANSLATION MISSING +|          relay-api.c | Main API functions for HTTP REST API. +// TRANSLATION MISSING +|          relay-api-msg.c | Send JSON messages to clients. +// TRANSLATION MISSING +|          relay-api-protocol.c | HTTP REST API protocol. |       irc/ | IRC прокси. |          relay-irc.c | Главне IRC прокси функције. -|       weechat/ | Релеј за удаљене интерфејсе. +// TRANSLATION MISSING +|       weechat/ | Relay for remote interfaces (using "weechat" binary protocol). |          relay-weechat.c | Релеј за удаљене интерфејсе (главне функције). |          relay-weechat-msg.c | Слање бинарних порука клијентима. |          relay-weechat-nicklist.c | Функције листе надимака. @@ -471,6 +482,8 @@ WeeChat „језгро” се налази у следећим директо |             test-typing-status.cpp | Тестови: typing статус. |          relay/ | Корен unit тестова за Релеј додатак. |             test-relay-auth.cpp | Тестови: аутентификација клијената. +// TRANSLATION MISSING +|             test-relay-http.cpp | Tests: HTTP functions for Relay plugin. |             irc/ | Корен unit тестова за Релеј „irc” протокол. |                test-relay-irc.cpp | Тестови: РЕлеј „irc” протокол. |          xfer/ | Корен unit тестова за Xfer додатак. diff --git a/doc/sr/weechat_user.sr.adoc b/doc/sr/weechat_user.sr.adoc index 448de318d..4617c7904 100644 --- a/doc/sr/weechat_user.sr.adoc +++ b/doc/sr/weechat_user.sr.adoc @@ -151,6 +151,10 @@ WeeChat мора да се изгради са CMake. | ca-certificates | | Сертификати за TLS везе. +// TRANSLATION MISSING +| libcjson-dev | +| Relay plugin: protocol "api" (HTTP REST API). + | libzstd-dev | ≥ 0.8.1 | Logger додатак: компресија ротирајућих лог фајлова (zstandard). + Relay додатак: компресија порука (WeeChat -> клијент) са https://facebook.github.io/zstd/[Zstandard ^↗^^] (weechat протокол). @@ -268,6 +272,10 @@ $ make install | ENABLE_CHARSET | `ON`, `OFF` | ON | Компајлира <>. +// TRANSLATION MISSING +| ENABLE_CJSON | `ON`, `OFF` | ON +| Support of JSON with https://github.com/DaveGamble/cJSON[cJSON ^↗^^] library. + | ENABLE_MAN | `ON`, `OFF` | OFF | Изграђује man страницу. diff --git a/po/srcfiles.cmake b/po/srcfiles.cmake index 1590120aa..b862f1f4e 100644 --- a/po/srcfiles.cmake +++ b/po/srcfiles.cmake @@ -344,6 +344,12 @@ SET(WEECHAT_SOURCES ./src/plugins/python/weechat-python-api.h ./src/plugins/python/weechat-python.c ./src/plugins/python/weechat-python.h +./src/plugins/relay/api/relay-api.c +./src/plugins/relay/api/relay-api.h +./src/plugins/relay/api/relay-api-msg.c +./src/plugins/relay/api/relay-api-msg.h +./src/plugins/relay/api/relay-api-protocol.c +./src/plugins/relay/api/relay-api-protocol.h ./src/plugins/relay/irc/relay-irc.c ./src/plugins/relay/irc/relay-irc.h ./src/plugins/relay/relay-auth.c @@ -360,6 +366,8 @@ SET(WEECHAT_SOURCES ./src/plugins/relay/relay-config.c ./src/plugins/relay/relay-config.h ./src/plugins/relay/relay.h +./src/plugins/relay/relay-http.c +./src/plugins/relay/relay-http.h ./src/plugins/relay/relay-info.c ./src/plugins/relay/relay-info.h ./src/plugins/relay/relay-network.c diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 53e3daa0d..49438bce1 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -95,6 +95,10 @@ if(ENABLE_ZSTD) include_directories(${ZSTD_INCLUDE_DIRS}) endif() +if(ENABLE_CJSON) + include_directories(${CJSON_INCLUDE_DIRS}) +endif() + include_directories("${CMAKE_BINARY_DIR}") add_library(weechat_core STATIC ${LIB_CORE_SRC}) target_link_libraries(weechat_core coverage_config) diff --git a/src/core/wee-debug.c b/src/core/wee-debug.c index 167870078..abf39e278 100644 --- a/src/core/wee-debug.c +++ b/src/core/wee-debug.c @@ -36,6 +36,9 @@ #ifdef HAVE_ZSTD #include #endif +#ifdef HAVE_CJSON +#include +#endif #include @@ -702,6 +705,16 @@ debug_libs_cb (const void *pointer, void *data, gui_chat_printf (NULL, " zstd: %s", _("not available")); #endif /* HAVE_ZSTD */ + /* display cJSON version */ +#ifdef HAVE_CJSON + gui_chat_printf (NULL, " cJSON: %d.%d.%d", + CJSON_VERSION_MAJOR, + CJSON_VERSION_MINOR, + CJSON_VERSION_PATCH); +#else + gui_chat_printf (NULL, " cJSON: %s", _("not available")); +#endif /* HAVE_CJSON */ + return WEECHAT_RC_OK; } diff --git a/src/plugins/relay/CMakeLists.txt b/src/plugins/relay/CMakeLists.txt index 890db888c..e467c384d 100644 --- a/src/plugins/relay/CMakeLists.txt +++ b/src/plugins/relay/CMakeLists.txt @@ -17,7 +17,7 @@ # along with WeeChat. If not, see . # -add_library(relay MODULE +set(RELAY_SRC relay.c relay.h relay-auth.c relay-auth.h relay-buffer.c relay-buffer.h @@ -25,6 +25,7 @@ add_library(relay MODULE relay-command.c relay-command.h relay-completion.c relay-completion.h relay-config.c relay-config.h + relay-http.c relay-http.h relay-info.c relay-info.h relay-network.c relay-network.h relay-raw.c relay-raw.h @@ -38,7 +39,19 @@ add_library(relay MODULE weechat/relay-weechat-msg.c weechat/relay-weechat-msg.h weechat/relay-weechat-nicklist.c weechat/relay-weechat-nicklist.h weechat/relay-weechat-protocol.c weechat/relay-weechat-protocol.h + ) + +if(ENABLE_CJSON) + list(APPEND RELAY_SRC + # API relay + api/relay-api.c api/relay-api.h + api/relay-api-msg.c api/relay-api-msg.h + api/relay-api-protocol.c api/relay-api-protocol.h + ) +endif() + +add_library(relay MODULE ${RELAY_SRC}) set_target_properties(relay PROPERTIES PREFIX "") set(LINK_LIBS) @@ -55,6 +68,11 @@ if(ENABLE_ZSTD) list(APPEND LINK_LIBS ${LIBZSTD_LDFLAGS}) endif() +if(ENABLE_CJSON) + include_directories(${CJSON_INCLUDE_DIRS}) + list(APPEND LINK_LIBS ${LIBCJSON_LDFLAGS}) +endif() + target_link_libraries(relay ${LINK_LIBS} coverage_config) install(TARGETS relay LIBRARY DESTINATION "${WEECHAT_LIBDIR}/plugins") diff --git a/src/plugins/relay/api/relay-api-msg.c b/src/plugins/relay/api/relay-api-msg.c new file mode 100644 index 000000000..df9c139cc --- /dev/null +++ b/src/plugins/relay/api/relay-api-msg.c @@ -0,0 +1,601 @@ +/* + * relay-api-msg.c - build JSON messages for "api" protocol + * + * Copyright (C) 2023-2024 Sébastien Helleu + * + * This file is part of WeeChat, the extensible chat client. + * + * WeeChat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * WeeChat is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with WeeChat. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include "../../weechat-plugin.h" +#include "../relay.h" +#include "../relay-client.h" +#include "../relay-http.h" +#include "relay-api.h" +#include "relay-api-msg.h" +#include "relay-api-protocol.h" + +#define MSG_ADD_STR_BUF(__json_name, __string) \ + cJSON_AddItemToObject( \ + json, __json_name, \ + cJSON_CreateString (__string)); + +#define MSG_ADD_STR_PTR(__json_name, __string) \ + cJSON_AddItemToObject( \ + json, __json_name, \ + cJSON_CreateString ((__string) ? __string : "")); + +#define MSG_ADD_HDATA_VAR(__json_type, __json_name, \ + __var_type, __var_name) \ + cJSON_AddItemToObject( \ + json, __json_name, \ + cJSON_Create##__json_type ( \ + weechat_hdata_##__var_type (hdata, pointer, __var_name))); + +#define MSG_ADD_HDATA_TIME(__json_name, __var_name) \ + date = weechat_hdata_time (hdata, pointer, __var_name); \ + strftime (str_time, sizeof (str_time), "%FT%TZ", gmtime (&date)); \ + MSG_ADD_STR_BUF(__json_name, str_time); + +#define MSG_ADD_HDATA_TIME_USEC(__json_name, \ + __var_name, __var_name_usec) \ + time_value = weechat_hdata_time (hdata, pointer, __var_name); \ + gmtime_r (&time_value, &gm_time); \ + tv.tv_sec = mktime (&gm_time); \ + tv.tv_usec = weechat_hdata_integer (hdata, pointer, \ + __var_name_usec); \ + weechat_util_strftimeval (str_time, sizeof (str_time), \ + "%FT%T.%fZ", &tv); \ + MSG_ADD_STR_BUF(__json_name, str_time); + +#define MSG_ADD_HDATA_STR(__json_name, __var_name) \ + ptr_string = weechat_hdata_string (hdata, pointer, __var_name); \ + MSG_ADD_STR_PTR(__json_name, ptr_string); + +#define MSG_ADD_HDATA_STR_COLORS(__json_name, __var_name) \ + ptr_string = weechat_hdata_string (hdata, pointer, __var_name); \ + switch (colors) \ + { \ + case RELAY_API_COLORS_ANSI: \ + string = weechat_hook_modifier_exec ( \ + "color_encode_ansi", NULL, \ + (ptr_string) ? ptr_string : ""); \ + if (string) \ + { \ + MSG_ADD_STR_PTR(__json_name, string); \ + free (string); \ + } \ + break; \ + case RELAY_API_COLORS_WEECHAT: \ + MSG_ADD_STR_PTR(__json_name, ptr_string); \ + break; \ + case RELAY_API_COLORS_STRIP: \ + string = weechat_string_remove_color ( \ + (ptr_string) ? ptr_string : "", NULL); \ + if (string) \ + { \ + MSG_ADD_STR_PTR(__json_name, string); \ + free (string); \ + } \ + case RELAY_API_NUM_COLORS: \ + break; \ + } + + +/* + * Sends JSON response to client (internal use). + * + * Returns number of bytes sent to client, -1 if error. + */ + +int +relay_api_msg_send_json_internal (struct t_relay_client *client, + int return_code, + const char *message, + const char *event_name, + const char *event_type, + const char *event_context, + const char *headers, + cJSON *json_body) +{ + cJSON *json, *json_event; + int num_bytes; + char *string; + + if (!client || !message) + return -1; + + num_bytes = -1; + + if (client->websocket == RELAY_CLIENT_WEBSOCKET_READY) + { + /* + * with established websocket, we return JSON string instead of + * an HTTP response + */ + json = cJSON_CreateObject (); + if (json) + { + cJSON_AddItemToObject (json, "code", cJSON_CreateNumber (return_code)); + cJSON_AddItemToObject (json, "message", cJSON_CreateString (message)); + if (event_name) + { + json_event = cJSON_CreateObject (); + if (json_event) + { + cJSON_AddItemToObject ( + json_event, "name", + cJSON_CreateString ((event_name) ? event_name : "")); + cJSON_AddItemToObject ( + json_event, "type", + cJSON_CreateString ((event_type) ? event_type : "")); + cJSON_AddItemToObject ( + json_event, "context", + cJSON_CreateString ((event_context) ? event_context : "")); + cJSON_AddItemToObject (json, "event", json_event); + } + } + cJSON_AddItemToObject (json, "body", json_body); + string = cJSON_PrintUnformatted (json); + num_bytes = relay_client_send ( + client, + RELAY_CLIENT_MSG_STANDARD, + string, + (string) ? strlen (string) : 0, + NULL); /* raw_message */ + if (string) + free (string); + cJSON_DetachItemFromObject (json, "body"); + cJSON_Delete (json); + } + } + else + { + string = (json_body) ? cJSON_PrintUnformatted (json_body) : NULL; + num_bytes = relay_http_send_json (client, return_code, message, headers, + string); + if (string) + free (string); + } + + return num_bytes; +} + +/* + * Sends JSON response to client (internal use). + * + * Returns number of bytes sent to client, -1 if error. + */ + +int +relay_api_msg_send_json (struct t_relay_client *client, + int return_code, + const char *message, + cJSON *json_body) +{ + return relay_api_msg_send_json_internal (client, + return_code, + message, + NULL, /* event_name */ + NULL, /* event_type */ + NULL, /* event_context */ + NULL, /* headers */ + json_body); +} + +/* + * Sends JSON error to client. + * + * Returns number of bytes sent to client, -1 if error. + */ + +int +relay_api_msg_send_error_json (struct t_relay_client *client, + int return_code, + const char *message, + const char *headers, + const char *format, ...) +{ + cJSON *json; + int num_bytes, length; + char *error_msg, *str_json; + + if (!client || !message || !format) + return -1; + + weechat_va_format (format); + if (!vbuffer) + return -1; + + num_bytes = -1; + + if (client->websocket == RELAY_CLIENT_WEBSOCKET_READY) + { + /* + * with established websocket, we return JSON string instead of + * an HTTP response + */ + json = cJSON_CreateObject (); + if (json) + { + cJSON_AddItemToObject (json, "error", cJSON_CreateString (vbuffer)); + num_bytes = relay_api_msg_send_json_internal ( + client, + return_code, + message, + NULL, /* event_name */ + NULL, /* event_type */ + NULL, /* event_context */ + headers, + json); + cJSON_Delete (json); + } + } + else + { + error_msg = weechat_string_replace (vbuffer, "\"", "\\\""); + if (error_msg) + { + length = strlen (error_msg) + 64; + str_json = malloc (length); + if (str_json) + { + snprintf (str_json, length, "{\"error\": \"%s\"}", error_msg); + num_bytes = relay_http_send_json (client, return_code, message, + headers, str_json); + free (str_json); + } + free (error_msg); + } + } + + free (vbuffer); + return num_bytes; +} + +/* + * Sends event to the client. + * + * Returns number of bytes sent to client, -1 if error. + */ + +int +relay_api_msg_send_event (struct t_relay_client *client, + const char *name, + const char *type, + const char *context, + cJSON *json_body) +{ + return relay_api_msg_send_json_internal (client, + RELAY_API_HTTP_0_EVENT, + name, + type, + context, + NULL, /* headers */ + json_body); +} + +/* + * Adds a buffer local variable into a JSON object. + */ + +void +relay_api_msg_buffer_add_local_vars_cb (void *data, + struct t_hashtable *hashtable, + const void *key, + const void *value) +{ + cJSON *json; + + /* make C compiler happy */ + (void) hashtable; + + json = (cJSON *)data; + + cJSON_AddItemToObject ( + json, + (const char *)key, + cJSON_CreateString ((value) ? (const char *)value : "")); +} + +/* + * Creates a JSON object with a buffer. + */ + +cJSON * +relay_api_msg_buffer_to_json (struct t_gui_buffer *buffer, + long lines, + int nicks, + enum t_relay_api_colors colors) +{ + struct t_hdata *hdata; + struct t_gui_buffer *pointer; + cJSON *json, *json_local_vars, *json_lines, *json_nicks; + const char *ptr_string; + char *string; + + hdata = relay_hdata_buffer; + pointer = buffer; + + json = cJSON_CreateObject (); + if (!json) + return NULL; + + if (!buffer) + return json; + + MSG_ADD_HDATA_STR("name", "full_name"); + MSG_ADD_HDATA_STR("short_name", "short_name"); + MSG_ADD_HDATA_VAR(Number, "number", integer, "number"); + ptr_string = weechat_buffer_get_string (buffer, "type"); + MSG_ADD_STR_PTR("type", ptr_string); + MSG_ADD_HDATA_STR_COLORS("title", "title"); + + /* local_variables */ + json_local_vars = cJSON_CreateObject (); + if (json_local_vars) + { + weechat_hashtable_map ( + weechat_hdata_pointer (hdata, buffer, "local_variables"), + &relay_api_msg_buffer_add_local_vars_cb, + json_local_vars); + cJSON_AddItemToObject (json, "local_variables", json_local_vars); + } + + /* lines */ + if (lines != 0) + { + json_lines = relay_api_msg_lines_to_json (buffer, lines, colors); + if (json_lines) + cJSON_AddItemToObject (json, "lines", json_lines); + } + + /* nicks */ + if (nicks) + { + json_nicks = relay_api_msg_nick_group_to_json ( + weechat_hdata_pointer (hdata, buffer, "nicklist_root")); + if (json_nicks) + cJSON_AddItemToObject (json, "nicks", json_nicks); + } + + return json; +} + +/* + * Creates a JSON object with a buffer line data. + */ + +cJSON * +relay_api_msg_line_data_to_json (struct t_gui_line_data *line_data, + enum t_relay_api_colors colors) +{ + struct t_hdata *hdata; + struct t_gui_line_data *pointer; + cJSON *json, *json_tags; + const char *ptr_string; + char *string, str_time[256], str_var[64]; + int i, tags_count; + time_t time_value; + struct timeval tv; + struct tm gm_time; + + hdata = relay_hdata_line_data; + pointer = line_data; + + json = cJSON_CreateObject (); + if (!json) + return NULL; + + if (!line_data) + return json; + + MSG_ADD_HDATA_VAR(Number, "id", integer, "id"); + MSG_ADD_HDATA_VAR(Number, "index", integer, "y"); + MSG_ADD_HDATA_TIME_USEC("date", "date", "date_usec"); + MSG_ADD_HDATA_TIME_USEC("date_printed", "date_printed", "date_usec_printed"); + MSG_ADD_HDATA_VAR(Bool, "highlight", char, "highlight"); + MSG_ADD_HDATA_STR_COLORS("prefix", "prefix"); + MSG_ADD_HDATA_STR_COLORS("message", "message"); + + /* tags */ + json_tags = cJSON_CreateArray (); + if (json_tags) + { + tags_count = weechat_hdata_integer (hdata, line_data, "tags_count"); + for (i = 0; i < tags_count; i++) + { + snprintf (str_var, sizeof (str_var), "%d|tags_array", i); + cJSON_AddItemToArray ( + json_tags, + cJSON_CreateString (weechat_hdata_string (hdata, line_data, str_var))); + } + } + cJSON_AddItemToObject(json, "tags", json_tags); + + return json; +} + +/* + * Creates a JSON object with an array of buffer lines. + */ + +cJSON * +relay_api_msg_lines_to_json (struct t_gui_buffer *buffer, + long lines, + enum t_relay_api_colors colors) +{ + cJSON *json; + struct t_gui_lines *ptr_lines; + struct t_gui_line *ptr_line; + struct t_gui_line_data *ptr_line_data; + long i, count; + + json = cJSON_CreateArray (); + if (!json) + return NULL; + + if (lines == 0) + return json; + + ptr_lines = weechat_hdata_pointer (relay_hdata_buffer, buffer, "own_lines"); + if (!ptr_lines) + return json; + + if (lines < 0) + { + /* search start line from the last line */ + ptr_line = weechat_hdata_pointer (relay_hdata_lines, ptr_lines, "last_line"); + if (ptr_line) + { + for (i = 1; i < lines * -1; i++) + { + ptr_line = weechat_hdata_move (relay_hdata_line, ptr_line, -1); + if (!ptr_line) + break; + } + if (!ptr_line) + ptr_line = weechat_hdata_pointer (relay_hdata_lines, ptr_lines, "first_line"); + } + } + else + { + ptr_line = weechat_hdata_pointer (relay_hdata_lines, ptr_lines, "first_line"); + } + + if (!ptr_line) + return json; + + count = 0; + while (ptr_line) + { + ptr_line_data = weechat_hdata_pointer (relay_hdata_line, ptr_line, "data"); + if (ptr_line_data) + { + cJSON_AddItemToArray ( + json, + relay_api_msg_line_data_to_json (ptr_line_data, colors)); + } + count++; + if ((lines > 0) && (count >= lines)) + break; + ptr_line = weechat_hdata_move (relay_hdata_line, ptr_line, 1); + } + + return json; +} + +/* + * Creates a nick JSON object. + */ + +cJSON * +relay_api_msg_nick_to_json (struct t_gui_nick *nick) +{ + struct t_hdata *hdata; + struct t_gui_nick *pointer; + cJSON *json; + const char *ptr_string; + + hdata = relay_hdata_nick; + pointer = nick; + + json = cJSON_CreateObject (); + if (!json) + return NULL; + + if (!nick) + return json; + + MSG_ADD_HDATA_STR("prefix", "prefix"); + MSG_ADD_HDATA_STR("prefix_color", "prefix_color"); + MSG_ADD_HDATA_STR("name", "name"); + MSG_ADD_HDATA_STR("color", "color"); + + return json; +} + +/* + * Creates a nick group JSON object. + */ + +cJSON * +relay_api_msg_nick_group_to_json (struct t_gui_nick_group *nick_group) +{ + struct t_hdata *hdata; + struct t_gui_nick_group *pointer; + cJSON *json, *json_groups, *json_nicks; + const char *ptr_string; + struct t_gui_nick_group *ptr_group; + struct t_gui_nick *ptr_nick; + + hdata = relay_hdata_nick_group; + pointer = nick_group; + + json = cJSON_CreateObject (); + if (!json) + return NULL; + + if (!nick_group) + return json; + + MSG_ADD_HDATA_STR("name", "name"); + MSG_ADD_HDATA_STR("color", "color"); + + json_groups = cJSON_CreateArray (); + if (json_groups) + { + ptr_group = weechat_hdata_pointer (relay_hdata_nick_group, nick_group, "children"); + if (ptr_group) + { + while (ptr_group) + { + cJSON_AddItemToArray ( + json_groups, + relay_api_msg_nick_group_to_json (ptr_group)); + ptr_group = weechat_hdata_move (relay_hdata_nick_group, ptr_group, 1); + } + } + cJSON_AddItemToObject (json, "groups", json_groups); + } + + json_nicks = cJSON_CreateArray (); + if (json_nicks) + { + ptr_nick = weechat_hdata_pointer (relay_hdata_nick_group, nick_group, "nicks"); + if (ptr_nick) + { + while (ptr_nick) + { + cJSON_AddItemToArray ( + json_nicks, + relay_api_msg_nick_to_json (ptr_nick)); + ptr_nick = weechat_hdata_move (relay_hdata_nick, ptr_nick, 1); + } + } + cJSON_AddItemToObject (json, "nicks", json_nicks); + } + + return json; +} diff --git a/src/plugins/relay/api/relay-api-msg.h b/src/plugins/relay/api/relay-api-msg.h new file mode 100644 index 000000000..8a2b7835c --- /dev/null +++ b/src/plugins/relay/api/relay-api-msg.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023-2024 Sébastien Helleu + * + * This file is part of WeeChat, the extensible chat client. + * + * WeeChat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * WeeChat is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with WeeChat. If not, see . + */ + +#ifndef WEECHAT_PLUGIN_RELAY_API_MSG_H +#define WEECHAT_PLUGIN_RELAY_API_MSG_H + +enum t_relay_api_colors; + +extern int relay_api_msg_send_json (struct t_relay_client *client, + int return_code, + const char *message, + cJSON *json_body); +extern int relay_api_msg_send_error_json (struct t_relay_client *client, + int return_code, + const char *message, + const char *headers, + const char *format, ...); +extern int relay_api_msg_send_event (struct t_relay_client *client, + const char *name, + const char *type, + const char *context, + cJSON *json_body); +extern cJSON *relay_api_msg_buffer_to_json (struct t_gui_buffer *buffer, + long lines, + int nicks, + enum t_relay_api_colors colors); +extern cJSON *relay_api_msg_lines_to_json (struct t_gui_buffer *pointer, + long lines, + enum t_relay_api_colors colors); +extern cJSON *relay_api_msg_line_data_to_json (struct t_gui_line_data *line_data, + enum t_relay_api_colors colors); +extern cJSON *relay_api_msg_nick_to_json (struct t_gui_nick *nick); +extern cJSON *relay_api_msg_nick_group_to_json (struct t_gui_nick_group *nick_group); + +#endif /* WEECHAT_PLUGIN_RELAY_API_MSG_H */ diff --git a/src/plugins/relay/api/relay-api-protocol.c b/src/plugins/relay/api/relay-api-protocol.c new file mode 100644 index 000000000..02601c745 --- /dev/null +++ b/src/plugins/relay/api/relay-api-protocol.c @@ -0,0 +1,878 @@ +/* + * relay-api-protocol.c - API protocol for relay to client + * + * Copyright (C) 2023-2024 Sébastien Helleu + * + * This file is part of WeeChat, the extensible chat client. + * + * WeeChat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * WeeChat is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with WeeChat. If not, see . + */ + +#include +#include +#include +#include + +#include + +#include "../../weechat-plugin.h" +#include "../relay.h" +#include "../relay-buffer.h" +#include "../relay-client.h" +#include "../relay-config.h" +#include "../relay-http.h" +#include "relay-api.h" +#include "relay-api-msg.h" +#include "relay-api-protocol.h" + + +/* + * Checks authentication from client. + * + * Returns: + * 1: OK, client authenticated + * 0: client NOT authenticated + */ + +int +relay_api_protocol_check_auth (struct t_relay_client *client, + struct t_relay_http_request *request) +{ + if (client->status == RELAY_STATUS_CONNECTED) + return 1; + + switch (relay_http_check_auth (request)) + { + case 0: /* OK */ + return 1; + case -1: /* missing password */ + relay_api_msg_send_error_json (client, + RELAY_HTTP_401_UNAUTHORIZED, + "WWW-Authenticate: Basic realm=Password", + RELAY_HTTP_ERROR_MISSING_PASSWORD); + break; + case -2: /* invalid password */ + relay_api_msg_send_error_json (client, + RELAY_HTTP_401_UNAUTHORIZED, + NULL, + RELAY_HTTP_ERROR_INVALID_PASSWORD); + break; + case -3: /* missing TOTP */ + relay_api_msg_send_error_json (client, + RELAY_HTTP_401_UNAUTHORIZED, + NULL, + RELAY_HTTP_ERROR_MISSING_TOTP); + break; + case -4: /* invalid TOTP */ + relay_api_msg_send_error_json (client, + RELAY_HTTP_401_UNAUTHORIZED, + NULL, + RELAY_HTTP_ERROR_INVALID_TOTP); + break; + case -5: /* out of memory */ + relay_api_msg_send_error_json (client, + RELAY_HTTP_503_SERVICE_UNAVAILABLE, + NULL, + RELAY_HTTP_ERROR_OUT_OF_MEMORY); + break; + } + + return 0; +} + +/* + * Returns value of an URL parameter as boolean (0 or 1), using a default value + * if the parameter is not set or if it's not a valid boolean. + */ + +int +relay_api_protocol_get_param_boolean (struct t_relay_http_request *request, + const char *name, + int default_value) +{ + const char *ptr_value; + + ptr_value = weechat_hashtable_get (request->params, name); + if (!ptr_value) + return default_value; + + return weechat_config_string_to_boolean (ptr_value); +} + +/* + * Returns value of an URL parameter as long, using a default value if the + * parameter is not set or if it's not a valid long integer. + */ + +long +relay_api_protocol_get_param_long (struct t_relay_http_request *request, + const char *name, + long default_value) +{ + const char *ptr_value; + char *error; + long number; + + ptr_value = weechat_hashtable_get (request->params, name); + if (!ptr_value) + return default_value; + + number = strtol (ptr_value, &error, 10); + if (error && !error[0]) + return number; + + return default_value; +} + +/* + * Callback for signals "buffer_*". + */ + +int +relay_api_protocol_signal_buffer_cb (const void *pointer, void *data, + const char *signal, + const char *type_data, + void *signal_data) +{ + struct t_relay_client *ptr_client; + struct t_gui_buffer *ptr_buffer; + struct t_gui_line *ptr_line; + struct t_gui_line_data *ptr_line_data; + cJSON *json; + long lines; + + /* make C compiler happy */ + (void) data; + (void) type_data; + + ptr_client = (struct t_relay_client *)pointer; + if (!ptr_client || !relay_client_valid (ptr_client)) + return WEECHAT_RC_OK; + + if ((strcmp (signal, "buffer_opened") == 0) + || (strcmp (signal, "buffer_type_changed") == 0) + || (strcmp (signal, "buffer_moved") == 0) + || (strcmp (signal, "buffer_merged") == 0) + || (strcmp (signal, "buffer_unmerged") == 0) + || (strcmp (signal, "buffer_hidden") == 0) + || (strcmp (signal, "buffer_unhidden") == 0) + || (strcmp (signal, "buffer_renamed") == 0) + || (strcmp (signal, "buffer_title_changed") == 0) + || (strncmp (signal, "buffer_localvar_", 16) == 0) + || (strcmp (signal, "buffer_cleared") == 0) + || (strcmp (signal, "buffer_closing") == 0)) + { + ptr_buffer = (struct t_gui_buffer *)signal_data; + if (!ptr_buffer) + return WEECHAT_RC_OK; + + lines = (strcmp (signal, "buffer_opened") == 0) ? LONG_MIN : 0; + json = relay_api_msg_buffer_to_json ( + ptr_buffer, lines, 0, RELAY_API_DATA(ptr_client, sync_colors)); + if (json) + { + relay_api_msg_send_event (ptr_client, signal, "buffer", "", json); + cJSON_Delete (json); + } + } + else if (strcmp (signal, "buffer_line_added") == 0) + { + ptr_line = (struct t_gui_line *)signal_data; + if (!ptr_line) + return WEECHAT_RC_OK; + + ptr_line_data = weechat_hdata_pointer (relay_hdata_line, + ptr_line, "data"); + if (!ptr_line_data) + return WEECHAT_RC_OK; + + ptr_buffer = weechat_hdata_pointer (relay_hdata_line_data, + ptr_line_data, "buffer"); + if (!ptr_buffer || relay_buffer_is_relay (ptr_buffer)) + return WEECHAT_RC_OK; + + json = relay_api_msg_line_data_to_json ( + ptr_line_data, RELAY_API_DATA(ptr_client, sync_colors)); + if (json) + { + relay_api_msg_send_event ( + ptr_client, + signal, + "line", + weechat_buffer_get_string (ptr_buffer, "full_name"), + json); + cJSON_Delete (json); + } + } + + return WEECHAT_RC_OK; +} + +/* + * Callback for hsignals "nicklist_*". + */ + +int +relay_api_protocol_hsignal_nicklist_cb (const void *pointer, void *data, + const char *signal, + struct t_hashtable *hashtable) +{ + struct t_relay_client *ptr_client; + struct t_gui_buffer *ptr_buffer; + struct t_gui_nick_group *ptr_parent_group, *ptr_group; + struct t_gui_nick *ptr_nick; + cJSON *json; + + /* make C compiler happy */ + (void) data; + + ptr_client = (struct t_relay_client *)pointer; + if (!ptr_client || !relay_client_valid (ptr_client)) + return WEECHAT_RC_OK; + + ptr_buffer = weechat_hashtable_get (hashtable, "buffer"); + ptr_parent_group = weechat_hashtable_get (hashtable, "parent_group"); + ptr_group = weechat_hashtable_get (hashtable, "group"); + ptr_nick = weechat_hashtable_get (hashtable, "nick"); + + /* if there is no parent group (for example "root" group), ignore the signal */ + if (!ptr_parent_group) + return WEECHAT_RC_OK; + + if ((strcmp (signal, "nicklist_group_added") == 0) + || (strcmp (signal, "nicklist_group_changed") == 0) + || (strcmp (signal, "nicklist_group_removing") == 0)) + { + json = relay_api_msg_nick_group_to_json (ptr_group); + if (json) + { + relay_api_msg_send_event ( + ptr_client, + signal, + "nick_group", + weechat_buffer_get_string (ptr_buffer, "full_name"), + json); + cJSON_Delete (json); + } + } + else if ((strcmp (signal, "nicklist_nick_added") == 0) + || (strcmp (signal, "nicklist_nick_changed") == 0) + || (strcmp (signal, "nicklist_nick_removing") == 0)) + { + json = relay_api_msg_nick_to_json (ptr_nick); + if (json) + { + relay_api_msg_send_event ( + ptr_client, + signal, + "nick", + weechat_buffer_get_string (ptr_buffer, "full_name"), + json); + cJSON_Delete (json); + } + } + + return WEECHAT_RC_OK; +} + +/* + * Callback for signals "upgrade*". + */ + +int +relay_api_protocol_signal_upgrade_cb (const void *pointer, void *data, + const char *signal, + const char *type_data, + void *signal_data) +{ + struct t_relay_client *ptr_client; + + /* make C compiler happy */ + (void) data; + (void) type_data; + (void) signal_data; + + ptr_client = (struct t_relay_client *)pointer; + if (!ptr_client || !relay_client_valid (ptr_client)) + return WEECHAT_RC_OK; + + if ((strcmp (signal, "upgrade") == 0) + || (strcmp (signal, "upgrade_ended") == 0)) + { + relay_api_msg_send_event (ptr_client, signal, NULL, NULL, NULL); + } + + return WEECHAT_RC_OK; +} + +/* + * Callback for resource "version". + * + * Routes: + * GET /api/version + */ + +RELAY_API_PROTOCOL_CALLBACK(version) +{ + cJSON *json; + char *version, *error; + long number; + + /* make C compiler happy */ + (void) request; + + json = cJSON_CreateObject (); + if (!json) + return WEECHAT_RC_ERROR; + + version = weechat_info_get ("version", NULL); + cJSON_AddItemToObject (json, + "weechat_version", cJSON_CreateString (version)); + free (version); + + version = weechat_info_get ("version_git", NULL); + cJSON_AddItemToObject (json, + "weechat_version_git", cJSON_CreateString (version)); + free (version); + + version = weechat_info_get ("version_number", NULL); + error = NULL; + number = strtol (version, &error, 10); + if (error && !error[0]) + { + cJSON_AddItemToObject (json, + "weechat_version_number", + cJSON_CreateNumber (number)); + } + free (version); + + cJSON_AddItemToObject (json, "relay_api_version", + cJSON_CreateString (RELAY_API_VERSION_STR)); + cJSON_AddItemToObject (json, + "relay_api_version_number", + cJSON_CreateNumber (RELAY_API_VERSION_NUMBER)); + + relay_api_msg_send_json (client, RELAY_HTTP_200_OK, json); + + cJSON_Delete (json); + + return WEECHAT_RC_OK; +} + +/* + * Callback for resource "buffers". + * + * Routes: + * GET /api/buffers + * GET /api/buffers/{buffer_name} + * GET /api/buffers/{buffer_name}/lines + * GET /api/buffers/{buffer_name}/lines/{line_id} + * GET /api/buffers/{buffer_name}/nicks + */ + +RELAY_API_PROTOCOL_CALLBACK(buffers) +{ + cJSON *json; + struct t_gui_buffer *ptr_buffer; + long lines; + int nicks; + enum t_relay_api_colors colors; + + ptr_buffer = NULL; + if (request->num_path_items > 2) + { + ptr_buffer = weechat_buffer_search ("==", request->path_items[2]); + if (!ptr_buffer) + { + relay_api_msg_send_error_json (client, RELAY_HTTP_404_NOT_FOUND, NULL, + "Buffer \"%s\" not found", + request->path_items[2]); + return WEECHAT_RC_OK; + } + } + + nicks = relay_api_protocol_get_param_boolean (request, "nicks", 0); + colors = relay_api_search_colors ( + weechat_hashtable_get (request->params, "colors")); + + if (request->num_path_items > 3) + { + /* sub-resource of buffers */ + if (strcmp (request->path_items[3], "lines") == 0) + { + lines = relay_api_protocol_get_param_long (request, "lines", -100L); + json = relay_api_msg_lines_to_json (ptr_buffer, lines, colors); + } + else if (strcmp (request->path_items[3], "nicks") == 0) + { + json = relay_api_msg_nick_group_to_json ( + weechat_hdata_pointer (relay_hdata_buffer, + ptr_buffer, "nicklist_root")); + } + else + { + relay_api_msg_send_error_json ( + client, RELAY_HTTP_404_NOT_FOUND, NULL, + "Sub-resource of buffers not found: \"%s\"", + request->path_items[3]); + return WEECHAT_RC_OK; + } + } + else + { + lines = relay_api_protocol_get_param_long (request, "lines", 0L); + if (ptr_buffer) + { + json = relay_api_msg_buffer_to_json (ptr_buffer, lines, nicks, colors); + } + else + { + json = cJSON_CreateArray (); + if (!json) + return WEECHAT_RC_ERROR; + ptr_buffer = weechat_hdata_get_list (relay_hdata_buffer, + "gui_buffers"); + while (ptr_buffer) + { + cJSON_AddItemToArray ( + json, + relay_api_msg_buffer_to_json (ptr_buffer, + lines, nicks, colors)); + ptr_buffer = weechat_hdata_move (relay_hdata_buffer, ptr_buffer, 1); + } + } + } + + if (!json) + goto error; + + relay_api_msg_send_json (client, RELAY_HTTP_200_OK, json); + cJSON_Delete (json); + return WEECHAT_RC_OK; + +error: + relay_api_msg_send_error_json (client, + RELAY_HTTP_503_SERVICE_UNAVAILABLE, + NULL, + RELAY_HTTP_ERROR_OUT_OF_MEMORY); + return WEECHAT_RC_OK; +} + +/* + * Callback for resource "input". + * + * Routes: + * POST /api/input/{buffer_name} + */ + +RELAY_API_PROTOCOL_CALLBACK(input) +{ + cJSON *json_body, *json_buffer, *json_command; + const char *ptr_buffer_name, *ptr_command, *ptr_weechat_commands; + struct t_gui_buffer *ptr_buffer; + struct t_hashtable *options; + + json_body = cJSON_Parse(request->body); + if (!json_body) + return WEECHAT_RC_ERROR; + + json_buffer = cJSON_GetObjectItem (json_body, "buffer"); + if (json_buffer) + { + ptr_buffer_name = cJSON_GetStringValue (json_buffer); + ptr_buffer = weechat_buffer_search ("==", ptr_buffer_name); + if (!ptr_buffer) + { + cJSON_Delete (json_body); + relay_api_msg_send_error_json (client, + RELAY_HTTP_404_NOT_FOUND, NULL, + "Buffer \"%s\" not found", + ptr_buffer_name); + return WEECHAT_RC_OK; + } + } + else + { + ptr_buffer = weechat_buffer_search_main (); + } + if (!ptr_buffer) + { + cJSON_Delete (json_body); + return WEECHAT_RC_ERROR; + } + + json_command = cJSON_GetObjectItem (json_body, "command"); + if (!json_command) + { + cJSON_Delete (json_body); + return WEECHAT_RC_ERROR; + } + + ptr_command = cJSON_GetStringValue (json_command); + if (!ptr_command) + { + cJSON_Delete (json_body); + return WEECHAT_RC_ERROR; + } + + options = weechat_hashtable_new (8, + WEECHAT_HASHTABLE_STRING, + WEECHAT_HASHTABLE_STRING, + NULL, NULL); + if (!options) + { + relay_api_msg_send_error_json (client, + RELAY_HTTP_503_SERVICE_UNAVAILABLE, + NULL, + RELAY_HTTP_ERROR_OUT_OF_MEMORY); + cJSON_Delete (json_body); + return WEECHAT_RC_OK; + } + + ptr_weechat_commands = weechat_config_string ( + relay_config_weechat_commands); + if (ptr_weechat_commands && ptr_weechat_commands[0]) + { + weechat_hashtable_set ( + options, + "commands", + weechat_config_string (relay_config_weechat_commands)); + } + + /* + * delay the execution of command after we go back in the WeeChat + * main loop (some commands like /upgrade executed now can cause + * a crash) + */ + weechat_hashtable_set (options, "delay", "1"); + + /* execute the command, with the delay */ + weechat_command_options (ptr_buffer, ptr_command, options); + + weechat_hashtable_free (options); + cJSON_Delete (json_body); + + relay_api_msg_send_json (client, RELAY_HTTP_204_NO_CONTENT, NULL); + + return WEECHAT_RC_OK; +} + +/* + * Callback for resource "ping". + * + * Routes: + * POST /api/ping + */ + +RELAY_API_PROTOCOL_CALLBACK(ping) +{ + cJSON *json, *json_body, *json_data; + const char *ptr_data; + + ptr_data = NULL; + json_body = cJSON_Parse(request->body); + if (json_body) + { + json_data = cJSON_GetObjectItem (json_body, "data"); + if (json_data && cJSON_IsString (json_data)) + ptr_data = cJSON_GetStringValue (json_data); + } + + if (ptr_data) + { + json = cJSON_CreateObject (); + if (!json) + { + cJSON_Delete (json_body); + return WEECHAT_RC_ERROR; + } + cJSON_AddItemToObject (json, "data", + cJSON_CreateString ((ptr_data) ? ptr_data : "")); + relay_api_msg_send_json (client, RELAY_HTTP_200_OK, json); + cJSON_Delete (json); + cJSON_Delete (json_body); + } + else + { + relay_api_msg_send_json (client, RELAY_HTTP_204_NO_CONTENT, NULL); + } + + return WEECHAT_RC_OK; +} + +/* + * Callback for resource "sync". + * + * Routes: + * POST /api/sync + */ + +RELAY_API_PROTOCOL_CALLBACK(sync) +{ + cJSON *json_body, *json_sync, *json_nicks, *json_colors; + + if (client->websocket != RELAY_CLIENT_WEBSOCKET_READY) + { + relay_api_msg_send_error_json ( + client, + RELAY_HTTP_403_FORBIDDEN, + NULL, + "Sync resource is available only with a websocket connection"); + return WEECHAT_RC_OK; + } + + RELAY_API_DATA(client, sync_enabled) = 1; + RELAY_API_DATA(client, sync_nicks) = 1; + RELAY_API_DATA(client, sync_colors) = RELAY_API_COLORS_ANSI; + + json_body = cJSON_Parse(request->body); + if (json_body) + { + json_sync = cJSON_GetObjectItem (json_body, "sync"); + if (json_sync && cJSON_IsBool (json_sync)) + RELAY_API_DATA(client, sync_enabled) = (cJSON_IsTrue (json_sync)) ? 1 : 0; + json_nicks = cJSON_GetObjectItem (json_body, "nicks"); + if (json_nicks && cJSON_IsBool (json_nicks)) + RELAY_API_DATA(client, sync_nicks) = (cJSON_IsTrue (json_nicks)) ? 1 : 0; + json_colors = cJSON_GetObjectItem (json_body, "colors"); + if (json_colors && cJSON_IsString (json_colors)) + RELAY_API_DATA(client, sync_colors) = relay_api_search_colors ( + cJSON_GetStringValue (json_colors)); + } + + if (RELAY_API_DATA(client, sync_enabled)) + relay_api_hook_signals (client); + else + relay_api_unhook_signals (client); + + relay_api_msg_send_json (client, RELAY_HTTP_204_NO_CONTENT, NULL); + + return WEECHAT_RC_OK; +} + +/* + * Reads a HTTP request from a client. + */ + +void +relay_api_protocol_recv_http (struct t_relay_client *client, + struct t_relay_http_request *request) +{ + int i, return_code, num_args; + struct t_relay_api_protocol_cb protocol_cb[] = + { { "GET", "version", 0, 0, &relay_api_protocol_cb_version }, + { "GET", "buffers", 0, 3, &relay_api_protocol_cb_buffers }, + { "POST", "input", 0, 0, &relay_api_protocol_cb_input }, + { "POST", "ping", 0, 0, &relay_api_protocol_cb_ping }, + { "POST", "sync", 0, 0, &relay_api_protocol_cb_sync }, + { NULL, NULL, 0, 0, NULL }, + }; + + if (!request || RELAY_CLIENT_HAS_ENDED(client)) + return; + + if (!relay_api_protocol_check_auth (client, request)) + { + relay_client_set_status (client, RELAY_STATUS_AUTH_FAILED); + return; + } + + /* display debug message */ + if (weechat_relay_plugin->debug >= 2) + { + weechat_printf (NULL, + "%s: recv from client %s%s%s: \"%s %s\", body: \"%s\"", + RELAY_PLUGIN_NAME, + RELAY_COLOR_CHAT_CLIENT, + client->desc, + RELAY_COLOR_CHAT, + request->method, + request->path, + request->body); + } + + if ((request->num_path_items < 2) || !request->path_items + || !request->path_items[0] || !request->path_items[1]) + { + goto error404; + } + + if (strcmp (request->path_items[0], "api") != 0) + goto error404; + + num_args = request->num_path_items - 2; + + for (i = 0; protocol_cb[i].resource; i++) + { + if ((strcmp (protocol_cb[i].method, request->method) == 0) + && (strcmp (protocol_cb[i].resource, request->path_items[1]) == 0)) + { + if (num_args < protocol_cb[i].min_args) + { + if (weechat_relay_plugin->debug >= 1) + { + weechat_printf ( + NULL, + _("%s%s: too few arguments received from client " + "%s%s%s for resource \"%s\" " + "(received: %d arguments, expected: at least %d)"), + weechat_prefix ("error"), + RELAY_PLUGIN_NAME, + RELAY_COLOR_CHAT_CLIENT, + client->desc, + RELAY_COLOR_CHAT, + request->path_items[1], + num_args, + protocol_cb[i].min_args); + } + goto error404; + } + if (num_args > protocol_cb[i].max_args) + { + if (weechat_relay_plugin->debug >= 1) + { + weechat_printf ( + NULL, + _("%s%s: too many arguments received from client " + "%s%s%s for resource \"%s\" " + "(received: %d arguments, expected: at most %d)"), + weechat_prefix ("error"), + RELAY_PLUGIN_NAME, + RELAY_COLOR_CHAT_CLIENT, + client->desc, + RELAY_COLOR_CHAT, + request->path_items[1], + num_args, + protocol_cb[i].max_args); + } + goto error404; + } + return_code = (int) (protocol_cb[i].cmd_function) ( + client, request); + if (return_code == WEECHAT_RC_OK) + return; + else + goto error400; + } + } + + goto error404; + +error400: + relay_api_msg_send_json (client, RELAY_HTTP_400_BAD_REQUEST, NULL); + goto error; + +error404: + relay_api_msg_send_json (client, RELAY_HTTP_404_NOT_FOUND, NULL); + goto error; + +error: + if (weechat_relay_plugin->debug >= 1) + { + weechat_printf (NULL, + _("%s%s: failed to execute route \"%s %s\" " + "for client %s%s%s"), + weechat_prefix ("error"), + RELAY_PLUGIN_NAME, + request->method, + request->path, + RELAY_COLOR_CHAT_CLIENT, + client->desc, + RELAY_COLOR_CHAT); + } +} + +/* + * Reads JSON string from a client: when connected via websocket (persistent + * connection), the client is sending JSON data as a request, which is + * converted to HTTP request by this function, before calling the function + * relay_api_protocol_recv_http. + * + * Example of JSON received: + * + * { + * "request": "POST /api/input", + * "body": { + * "buffer": "irc.libera.#weechat", + * "command": "hello!" + * } + * } + * + * It is converted to an HTTP request which could have been: + * + * POST /api/input HTTP/1.1 + * Content-Length: 53 + * Content-Type: application/json + * + * {"buffer": "irc.libera.#weechat","command": "hello!"} + */ + +void +relay_api_protocol_recv_json (struct t_relay_client *client, const char *json) +{ + cJSON *json_obj, *json_request, *json_body; + char *string_body; + int length; + struct t_relay_http_request *request; + + request = NULL; + + json_obj = cJSON_Parse(json); + if (!json_obj) + goto error; + + json_request = cJSON_GetObjectItem (json_obj, "request"); + if (!json_request) + goto error; + + request = relay_http_request_alloc (); + if (!request) + goto error; + + if (!relay_http_parse_method_path (request, + cJSON_GetStringValue (json_request))) + { + goto error; + } + + json_body = cJSON_GetObjectItem (json_obj, "body"); + if (json_body) + { + string_body = cJSON_PrintUnformatted (json_body); + if (string_body) + { + length = strlen (string_body); + request->body = malloc (length + 1); + if (request->body) + { + memcpy (request->body, string_body, length + 1); + request->content_length = length; + request->body_size = length; + } + free (string_body); + } + } + + relay_api_protocol_recv_http (client, request); + goto end; + +error: + relay_api_msg_send_json (client, RELAY_HTTP_400_BAD_REQUEST, NULL); + +end: + if (json_obj) + cJSON_Delete (json_obj); + if (request) + relay_http_request_free (request); +} diff --git a/src/plugins/relay/api/relay-api-protocol.h b/src/plugins/relay/api/relay-api-protocol.h new file mode 100644 index 000000000..df1be074a --- /dev/null +++ b/src/plugins/relay/api/relay-api-protocol.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023-2024 Sébastien Helleu + * + * This file is part of WeeChat, the extensible chat client. + * + * WeeChat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * WeeChat is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with WeeChat. If not, see . + */ + +#ifndef WEECHAT_PLUGIN_RELAY_API_PROTOCOL_H +#define WEECHAT_PLUGIN_RELAY_API_PROTOCOL_H + +#define RELAY_API_PROTOCOL_CALLBACK(__command) \ + int \ + relay_api_protocol_cb_##__command ( \ + struct t_relay_client *client, \ + struct t_relay_http_request *request) + +struct t_relay_api_protocol_ctxt +{ + struct t_relay_client *client; + struct t_relay_http_request *request; + char *resource; +}; + +typedef int (t_relay_api_cmd_func)(struct t_relay_client *client, + struct t_relay_http_request *request); + +struct t_relay_api_protocol_cb +{ + char *method; /* method (eg: "GET", "POST", etc.) */ + char *resource; /* resource (eg: "buffers") */ + int min_args; /* min number of items in path */ + int max_args; /* max number of items in path */ + t_relay_api_cmd_func *cmd_function; /* callback */ +}; + +extern int relay_api_protocol_signal_buffer_cb (const void *pointer, + void *data, + const char *signal, + const char *type_data, + void *signal_data); +extern int relay_api_protocol_hsignal_nicklist_cb (const void *pointer, + void *data, + const char *signal, + struct t_hashtable *hashtable); +extern int relay_api_protocol_signal_upgrade_cb (const void *pointer, + void *data, + const char *signal, + const char *type_data, + void *signal_data); +extern void relay_api_protocol_recv_http (struct t_relay_client *client, + struct t_relay_http_request *request); +extern void relay_api_protocol_recv_json (struct t_relay_client *client, + const char *json); + +#endif /* WEECHAT_PLUGIN_RELAY_API_PROTOCOL_H */ diff --git a/src/plugins/relay/api/relay-api.c b/src/plugins/relay/api/relay-api.c new file mode 100644 index 000000000..a82c72e0b --- /dev/null +++ b/src/plugins/relay/api/relay-api.c @@ -0,0 +1,311 @@ +/* + * relay-api.c - API protocol for relay to client + * + * Copyright (C) 2023-2024 Sébastien Helleu + * + * This file is part of WeeChat, the extensible chat client. + * + * WeeChat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * WeeChat is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with WeeChat. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../weechat-plugin.h" +#include "../relay.h" +#include "../relay-client.h" +#include "../relay-config.h" +#include "../relay-http.h" +#include "../relay-raw.h" +#include "../relay-raw.h" +#include "relay-api.h" +#include "relay-api-protocol.h" + + +/* + * Returns value of "colors" URL parameter, an enum with one of these values: + * - RELAY_API_COLORS_ANSI (default) + * - RELAY_API_COLORS_WEECHAT + * - RELAY_API_COLORS_STRIP + */ + +enum t_relay_api_colors +relay_api_search_colors (const char *colors) +{ + if (!colors) + return RELAY_API_COLORS_ANSI; + + if (strcmp (colors, "weechat") == 0) + return RELAY_API_COLORS_WEECHAT; + + if (strcmp (colors, "strip") == 0) + return RELAY_API_COLORS_STRIP; + + return RELAY_API_COLORS_ANSI; +} + +/* + * Hooks signals for a client. + */ + +void +relay_api_hook_signals (struct t_relay_client *client) +{ + if (!RELAY_API_DATA(client, hook_signal_buffer)) + { + RELAY_API_DATA(client, hook_signal_buffer) = + weechat_hook_signal ("buffer_*", + &relay_api_protocol_signal_buffer_cb, + client, NULL); + } + if (RELAY_API_DATA(client, sync_nicks)) + { + if (!RELAY_API_DATA(client, hook_hsignal_nicklist)) + { + RELAY_API_DATA(client, hook_hsignal_nicklist) = + weechat_hook_hsignal ("nicklist_*", + &relay_api_protocol_hsignal_nicklist_cb, + client, NULL); + } + } + else + { + if (RELAY_API_DATA(client, hook_hsignal_nicklist)) + { + weechat_unhook (RELAY_API_DATA(client, hook_hsignal_nicklist)); + RELAY_API_DATA(client, hook_hsignal_nicklist) = NULL; + } + } + if (!RELAY_API_DATA(client, hook_signal_upgrade)) + { + RELAY_API_DATA(client, hook_signal_upgrade) = + weechat_hook_signal ("upgrade*", + &relay_api_protocol_signal_upgrade_cb, + client, NULL); + } +} + +/* + * Unhooks signals for a client. + */ + +void +relay_api_unhook_signals (struct t_relay_client *client) +{ + if (RELAY_API_DATA(client, hook_signal_buffer)) + { + weechat_unhook (RELAY_API_DATA(client, hook_signal_buffer)); + RELAY_API_DATA(client, hook_signal_buffer) = NULL; + } + if (RELAY_API_DATA(client, hook_hsignal_nicklist)) + { + weechat_unhook (RELAY_API_DATA(client, hook_hsignal_nicklist)); + RELAY_API_DATA(client, hook_hsignal_nicklist) = NULL; + } + if (RELAY_API_DATA(client, hook_signal_upgrade)) + { + weechat_unhook (RELAY_API_DATA(client, hook_signal_upgrade)); + RELAY_API_DATA(client, hook_signal_upgrade) = NULL; + } +} + +/* + * Reads HTTP request from a client. + */ + +void +relay_api_recv_http (struct t_relay_client *client, + struct t_relay_http_request *request) +{ + relay_api_protocol_recv_http (client, request); +} + +/* + * Reads JSON string from a client. + */ + +void +relay_api_recv_json (struct t_relay_client *client, const char *json) +{ + relay_api_protocol_recv_json (client, json); +} + +/* + * Closes connection with a client. + */ + +void +relay_api_close_connection (struct t_relay_client *client) +{ + /* + * IMPORTANT: if changes are made in this function or sub-functions called, + * please also update the function relay_api_add_to_infolist: + * when the flag force_disconnected_state is set to 1 we simulate + * a disconnected state for client in infolist (used on /upgrade -save) + */ + + relay_api_unhook_signals (client); +} + +/* + * Initializes relay data specific to API protocol. + */ + +void +relay_api_alloc (struct t_relay_client *client) +{ + client->protocol_data = malloc (sizeof (struct t_relay_api_data)); + if (!client->protocol_data) + return; + + RELAY_API_DATA(client, hook_signal_buffer) = NULL; + RELAY_API_DATA(client, hook_hsignal_nicklist) = NULL; + RELAY_API_DATA(client, hook_signal_upgrade) = NULL; + RELAY_API_DATA(client, sync_enabled) = 0; + RELAY_API_DATA(client, sync_nicks) = 0; + RELAY_API_DATA(client, sync_colors) = RELAY_API_COLORS_ANSI; +} + +/* + * Initializes relay data specific to API protocol with an infolist. + * + * This is called after /upgrade. + */ + +void +relay_api_alloc_with_infolist (struct t_relay_client *client, + struct t_infolist *infolist) +{ + client->protocol_data = malloc (sizeof (struct t_relay_api_data)); + if (!client->protocol_data) + return; + + RELAY_API_DATA(client, hook_signal_buffer) = NULL; + RELAY_API_DATA(client, hook_hsignal_nicklist) = NULL; + RELAY_API_DATA(client, hook_signal_upgrade) = NULL; + RELAY_API_DATA(client, sync_enabled) = weechat_infolist_integer ( + infolist, "sync_enabled"); + RELAY_API_DATA(client, sync_nicks) = weechat_infolist_integer ( + infolist, "sync_nicks"); + RELAY_API_DATA(client, sync_colors) = weechat_infolist_integer ( + infolist, "sync_colors"); + + if (!RELAY_CLIENT_HAS_ENDED(client) && RELAY_API_DATA(client, sync_enabled)) + relay_api_hook_signals (client); +} + +/* + * Returns the client initial status: it is always "waiting_auth" for API + * protocol because we always expect the "init" command, even without any + * password. + */ + +enum t_relay_status +relay_api_get_initial_status (struct t_relay_client *client) +{ + /* make C compiler happy */ + (void) client; + + return RELAY_STATUS_WAITING_AUTH; +} + +/* + * Frees relay data specific to API protocol. + */ + +void +relay_api_free (struct t_relay_client *client) +{ + if (!client) + return; + + if (client->protocol_data) + { + if (RELAY_API_DATA(client, hook_signal_buffer)) + weechat_unhook (RELAY_API_DATA(client, hook_signal_buffer)); + if (RELAY_API_DATA(client, hook_hsignal_nicklist)) + weechat_unhook (RELAY_API_DATA(client, hook_hsignal_nicklist)); + if (RELAY_API_DATA(client, hook_signal_upgrade)) + weechat_unhook (RELAY_API_DATA(client, hook_signal_upgrade)); + + free (client->protocol_data); + + client->protocol_data = NULL; + } +} + +/* + * Adds client API data in an infolist. + * + * If force_disconnected_state == 1, the infolist contains the client + * in a disconnected state (but the client is unchanged, still connected if it + * was). + * + * Returns: + * 1: OK + * 0: error + */ + +int +relay_api_add_to_infolist (struct t_infolist_item *item, + struct t_relay_client *client, + int force_disconnected_state) +{ + if (!item || !client) + return 0; + + /* parameter not used today, it may be in future */ + (void) force_disconnected_state; + + if (!weechat_infolist_new_var_pointer (item, "hook_signal_buffer", RELAY_API_DATA(client, hook_signal_buffer))) + return 0; + if (!weechat_infolist_new_var_pointer (item, "hook_hsignal_nicklist", RELAY_API_DATA(client, hook_hsignal_nicklist))) + return 0; + if (!weechat_infolist_new_var_pointer (item, "hook_signal_upgrade", RELAY_API_DATA(client, hook_signal_upgrade))) + return 0; + if (!weechat_infolist_new_var_integer (item, "sync_enabled", RELAY_API_DATA(client, sync_enabled))) + return 0; + if (!weechat_infolist_new_var_integer (item, "sync_nicks", RELAY_API_DATA(client, sync_nicks))) + return 0; + if (!weechat_infolist_new_var_integer (item, "sync_colors", RELAY_API_DATA(client, sync_colors))) + return 0; + + return 1; +} + +/* + * Prints client API data in WeeChat log file (usually for crash dump). + */ + +void +relay_api_print_log (struct t_relay_client *client) +{ + if (client->protocol_data) + { + weechat_log_printf (" hook_signal_buffer. . . : 0x%lx", RELAY_API_DATA(client, hook_signal_buffer)); + weechat_log_printf (" hook_hsignal_nicklist . : 0x%lx", RELAY_API_DATA(client, hook_hsignal_nicklist)); + weechat_log_printf (" hook_signal_upgrade . . : 0x%lx", RELAY_API_DATA(client, hook_signal_upgrade)); + weechat_log_printf (" sync_enabled. . . . . . : %d", RELAY_API_DATA(client, sync_enabled)); + weechat_log_printf (" sync_nicks. . . . . . . : %d", RELAY_API_DATA(client, sync_nicks)); + weechat_log_printf (" sync_colors . . . . . . : %d", RELAY_API_DATA(client, sync_colors)); + } +} diff --git a/src/plugins/relay/api/relay-api.h b/src/plugins/relay/api/relay-api.h new file mode 100644 index 000000000..d3254aa55 --- /dev/null +++ b/src/plugins/relay/api/relay-api.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023-2024 Sébastien Helleu + * + * This file is part of WeeChat, the extensible chat client. + * + * WeeChat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * WeeChat is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with WeeChat. If not, see . + */ + +#ifndef WEECHAT_PLUGIN_RELAY_API_H +#define WEECHAT_PLUGIN_RELAY_API_H + +struct t_relay_client; +struct t_relay_http_request; +enum t_relay_status; + +#define RELAY_API_VERSION_MAJOR 0 +#define RELAY_API_VERSION_MINOR 0 +#define RELAY_API_VERSION_PATCH 1 +#define RELAY_API_VERSION_NUMBER \ + ((RELAY_API_VERSION_MAJOR << 16) \ + + (RELAY_API_VERSION_MINOR << 8) \ + + RELAY_API_VERSION_PATCH) +#define RELAY_API_VERSION_STR \ + (TO_STR(RELAY_API_VERSION_MAJOR) \ + "." \ + TO_STR(RELAY_API_VERSION_MINOR) \ + "." \ + TO_STR(RELAY_API_VERSION_PATCH)) + +#define RELAY_API_DATA(client, var) \ + (((struct t_relay_api_data *)client->protocol_data)->var) + +#define RELAY_API_HTTP_0_EVENT 0, "Event" + +enum t_relay_api_colors +{ + RELAY_API_COLORS_ANSI = 0, /* convert colors to ANSI colors */ + RELAY_API_COLORS_WEECHAT, /* keep WeeChat internal color codes */ + RELAY_API_COLORS_STRIP, /* strip colors */ + /* number of data types */ + RELAY_API_NUM_COLORS, +}; + +struct t_relay_api_data +{ + struct t_hook *hook_signal_buffer; /* hook for signals "buffer_*" */ + struct t_hook *hook_hsignal_nicklist; /* hook for hsignals "nicklist_*" */ + struct t_hook *hook_signal_upgrade; /* hook for signals "upgrade*" */ + int sync_enabled; /* 1 if sync is enabled */ + int sync_nicks; /* 1 if nicks are synchronized */ + enum t_relay_api_colors sync_colors; /* colors to send with sync */ +}; + +extern enum t_relay_api_colors relay_api_search_colors (const char *colors); +extern void relay_api_hook_signals (struct t_relay_client *client); +extern void relay_api_unhook_signals (struct t_relay_client *client); +extern void relay_api_recv_http (struct t_relay_client *client, + struct t_relay_http_request *request); +extern void relay_api_recv_json (struct t_relay_client *client, + const char *json); +extern void relay_api_close_connection (struct t_relay_client *client); +extern void relay_api_alloc (struct t_relay_client *client); +extern void relay_api_alloc_with_infolist (struct t_relay_client *client, + struct t_infolist *infolist); +extern enum t_relay_status relay_api_get_initial_status (struct t_relay_client *client); +extern void relay_api_free (struct t_relay_client *client); +extern int relay_api_add_to_infolist (struct t_infolist_item *item, + struct t_relay_client *client, + int force_disconnected_state); +extern void relay_api_print_log (struct t_relay_client *client); + +#endif /* WEECHAT_PLUGIN_RELAY_API_H */ diff --git a/src/plugins/relay/relay-client.c b/src/plugins/relay/relay-client.c index 5e4e30c02..1bcc8294f 100644 --- a/src/plugins/relay/relay-client.c +++ b/src/plugins/relay/relay-client.c @@ -37,10 +37,14 @@ #include "relay-auth.h" #include "relay-config.h" #include "relay-buffer.h" +#include "relay-http.h" #include "relay-network.h" #include "relay-raw.h" #include "relay-server.h" #include "relay-websocket.h" +#ifdef HAVE_CJSON +#include "api/relay-api.h" +#endif #include "irc/relay-irc.h" #include "weechat/relay-weechat.h" @@ -55,7 +59,7 @@ char *relay_client_status_name[] = /* name of status (for signal/info) */ }; char *relay_client_data_type_string[] = /* strings for data types */ -{ "text", "binary" }; +{ "text", "binary", "http" }; char *relay_client_msg_type_string[] = /* prefix in raw buffer for message */ { "", "[PING]\n", "[PONG]\n", "[CLOSE]\n" }; @@ -266,6 +270,13 @@ relay_client_handshake_timer_cb (const void *pointer, void *data, client, relay_irc_get_initial_status (client)); break; + case RELAY_PROTOCOL_API: +#ifdef HAVE_CJSON + relay_client_set_status ( + client, + relay_api_get_initial_status (client)); +#endif /* HAVE_CJSON */ + break; case RELAY_NUM_PROTOCOLS: break; } @@ -312,6 +323,124 @@ relay_client_handshake_timer_cb (const void *pointer, void *data, return WEECHAT_RC_OK; } +/* + * Reads text data from a client (messages on a single line): splits data on + * '\n' and keeps a partial message if data does not end with '\n'. + */ + +void +relay_client_recv_text_single_line (struct t_relay_client *client) +{ + char *pos, *raw_msg, **lines, *tmp; + int i, num_lines, length; + + if (!client->partial_message) + return; + + pos = strrchr (client->partial_message, '\n'); + if (!pos) + return; + + /* print message in raw buffer */ + raw_msg = weechat_strndup (client->partial_message, + pos - client->partial_message + 1); + if (raw_msg) + { + relay_raw_print (client, RELAY_CLIENT_MSG_STANDARD, + RELAY_RAW_FLAG_RECV, + raw_msg, strlen (raw_msg) + 1); + free (raw_msg); + } + + pos[0] = '\0'; + + lines = weechat_string_split (client->partial_message, "\n", NULL, + WEECHAT_STRING_SPLIT_STRIP_LEFT + | WEECHAT_STRING_SPLIT_STRIP_RIGHT + | WEECHAT_STRING_SPLIT_COLLAPSE_SEPS, + 0, &num_lines); + if (lines) + { + for (i = 0; i < num_lines; i++) + { + /* remove final '\r' */ + length = strlen (lines[i]); + if ((length > 0) && (lines[i][length - 1] == '\r')) + lines[i][length - 1] = '\0'; + + /* + * interpret text from client, according to the relay + * protocol used + */ + switch (client->protocol) + { + case RELAY_PROTOCOL_WEECHAT: + relay_weechat_recv (client, lines[i]); + break; + case RELAY_PROTOCOL_IRC: + relay_irc_recv (client, lines[i]); + break; + case RELAY_PROTOCOL_API: + /* api is multi-line only (JSON) */ + break; + case RELAY_NUM_PROTOCOLS: + break; + } + } + weechat_string_free_split (lines); + } + if (pos[1]) + { + tmp = strdup (pos + 1); + free (client->partial_message); + client->partial_message = tmp; + } + else + { + free (client->partial_message); + client->partial_message = NULL; + } +} + +/* + * Reads text data from a client (multi-line messages). + */ + +void +relay_client_recv_text_multi_line (struct t_relay_client *client) +{ + if (!client->partial_message) + return; + + /* print message in raw buffer */ + relay_raw_print (client, RELAY_CLIENT_MSG_STANDARD, + RELAY_RAW_FLAG_RECV, + client->partial_message, + strlen (client->partial_message) + 1); + + /* + * interpret text from client, according to the relay + * protocol used + */ + switch (client->protocol) + { + case RELAY_PROTOCOL_WEECHAT: + /* weechat is single line only */ + break; + case RELAY_PROTOCOL_IRC: + /* irc is single line only */ + break; + case RELAY_PROTOCOL_API: + relay_api_recv_json (client, client->partial_message); + break; + case RELAY_NUM_PROTOCOLS: + break; + } + + free (client->partial_message); + client->partial_message = NULL; +} + /* * Reads text data from a client: splits data on '\n' and keeps a partial * message if data does not end with '\n'. @@ -320,9 +449,7 @@ relay_client_handshake_timer_cb (const void *pointer, void *data, void relay_client_recv_text (struct t_relay_client *client, const char *data) { - char *new_partial, *raw_msg, **lines, *pos, *tmp, *handshake; - const char *ptr_real_ip; - int i, num_lines, length, rc; + char *new_partial; if (client->partial_message) { @@ -337,172 +464,12 @@ relay_client_recv_text (struct t_relay_client *client, const char *data) else client->partial_message = strdup (data); - pos = strrchr (client->partial_message, '\n'); - if (pos) + if (client->partial_message) { - /* print message in raw buffer */ - raw_msg = weechat_strndup (client->partial_message, - pos - client->partial_message + 1); - if (raw_msg) - { - relay_raw_print (client, RELAY_CLIENT_MSG_STANDARD, - RELAY_RAW_FLAG_RECV, - raw_msg, strlen (raw_msg) + 1); - free (raw_msg); - } - - pos[0] = '\0'; - - lines = weechat_string_split (client->partial_message, "\n", NULL, - WEECHAT_STRING_SPLIT_STRIP_LEFT - | WEECHAT_STRING_SPLIT_STRIP_RIGHT - | WEECHAT_STRING_SPLIT_COLLAPSE_SEPS, - 0, &num_lines); - if (lines) - { - for (i = 0; i < num_lines; i++) - { - /* remove final '\r' */ - length = strlen (lines[i]); - if ((length > 0) && (lines[i][length - 1] == '\r')) - lines[i][length - 1] = '\0'; - - /* if websocket is initializing */ - if (client->websocket == RELAY_CLIENT_WEBSOCKET_INITIALIZING) - { - if (lines[i][0]) - { - /* web socket is initializing, read HTTP headers */ - relay_websocket_save_header (client, lines[i]); - } - else - { - /* - * empty line means that we have received all HTTP - * headers: then we check the validity of websocket, and - * if it is OK, we'll do the handshake and answer to the - * client - */ - rc = relay_websocket_client_handshake_valid (client); - if (rc == 0) - { - /* handshake from client is valid */ - handshake = relay_websocket_build_handshake (client); - if (handshake) - { - relay_client_send (client, - RELAY_CLIENT_MSG_STANDARD, - handshake, - strlen (handshake), NULL); - free (handshake); - client->websocket = RELAY_CLIENT_WEBSOCKET_READY; - } - } - else - { - switch (rc) - { - case -1: - relay_websocket_send_http (client, - "400 Bad Request"); - if (weechat_relay_plugin->debug >= 1) - { - weechat_printf_date_tags ( - NULL, 0, "relay_client", - _("%s%s: invalid websocket " - "handshake received for client " - "%s%s%s"), - weechat_prefix ("error"), - RELAY_PLUGIN_NAME, - RELAY_COLOR_CHAT_CLIENT, - client->desc, - RELAY_COLOR_CHAT); - } - break; - case -2: - relay_websocket_send_http (client, - "403 Forbidden"); - if (weechat_relay_plugin->debug >= 1) - { - weechat_printf_date_tags ( - NULL, 0, "relay_client", - _("%s%s: origin \"%s\" " - "not allowed for websocket"), - weechat_prefix ("error"), - RELAY_PLUGIN_NAME, - weechat_hashtable_get (client->http_headers, - "origin")); - } - break; - } - relay_client_set_status (client, RELAY_STATUS_DISCONNECTED); - } - - ptr_real_ip = weechat_hashtable_get ( - client->http_headers, "x-real-ip"); - if (ptr_real_ip) - { - if (client->real_ip) - free (client->real_ip); - client->real_ip = strdup (ptr_real_ip); - relay_client_set_desc (client); - weechat_printf_date_tags ( - NULL, 0, "relay_client", - _("%s: websocket client %s%s%s has real IP " - "address \"%s\""), - RELAY_PLUGIN_NAME, - RELAY_COLOR_CHAT_CLIENT, - client->desc, - RELAY_COLOR_CHAT, - ptr_real_ip); - } - - /* remove HTTP headers */ - weechat_hashtable_free (client->http_headers); - client->http_headers = NULL; - - /* - * discard all received data after the handshake - * received from client, and return immediately - */ - free (client->partial_message); - client->partial_message = NULL; - weechat_string_free_split (lines); - return; - } - } - else - { - /* - * interpret text from client, according to the relay - * protocol used - */ - switch (client->protocol) - { - case RELAY_PROTOCOL_WEECHAT: - relay_weechat_recv (client, lines[i]); - break; - case RELAY_PROTOCOL_IRC: - relay_irc_recv (client, lines[i]); - break; - case RELAY_NUM_PROTOCOLS: - break; - } - } - } - weechat_string_free_split (lines); - } - if (pos[1]) - { - tmp = strdup (pos + 1); - free (client->partial_message); - client->partial_message = tmp; - } - else - { - free (client->partial_message); - client->partial_message = NULL; - } + if (client->recv_data_type == RELAY_CLIENT_DATA_TEXT_LINE) + relay_client_recv_text_single_line (client); + else if (client->recv_data_type == RELAY_CLIENT_DATA_TEXT_MULTILINE) + relay_client_recv_text_multi_line (client); } } @@ -566,7 +533,17 @@ relay_client_recv_text_buffer (struct t_relay_client *client, } if (msg_type == RELAY_CLIENT_MSG_STANDARD) - relay_client_recv_text (client, buffer + index); + { + if ((client->websocket == RELAY_CLIENT_WEBSOCKET_INITIALIZING) + || (client->recv_data_type == RELAY_CLIENT_DATA_HTTP)) + { + relay_http_recv (client, buffer + index); + } + else + { + relay_client_recv_text (client, buffer + index); + } + } index += strlen (buffer + index) + 1; } @@ -622,7 +599,7 @@ relay_client_recv_cb (const void *pointer, void *data, int fd) */ if (client->bytes_recv == 0) { - if (relay_websocket_is_http_get_weechat (buffer)) + if (relay_websocket_is_valid_http_get (client, buffer)) { /* * web socket is just initializing for now, it's not accepted @@ -630,11 +607,6 @@ relay_client_recv_cb (const void *pointer, void *data, int fd) * valid or not) */ client->websocket = RELAY_CLIENT_WEBSOCKET_INITIALIZING; - client->http_headers = weechat_hashtable_new ( - 32, - WEECHAT_HASHTABLE_STRING, - WEECHAT_HASHTABLE_STRING, - NULL, NULL); } } @@ -679,9 +651,11 @@ relay_client_recv_cb (const void *pointer, void *data, int fd) } if ((client->websocket == RELAY_CLIENT_WEBSOCKET_INITIALIZING) - || (client->recv_data_type == RELAY_CLIENT_DATA_TEXT)) + || (client->recv_data_type == RELAY_CLIENT_DATA_TEXT_LINE) + || (client->recv_data_type == RELAY_CLIENT_DATA_TEXT_MULTILINE) + || (client->recv_data_type == RELAY_CLIENT_DATA_HTTP)) { - /* websocket initializing or text data for this client */ + /* websocket initializing or text/HTTP data for this client */ relay_client_recv_text_buffer (client, ptr_buffer, length_buffer); } else @@ -698,17 +672,20 @@ relay_client_recv_cb (const void *pointer, void *data, int fd) if ((num_read == 0) || ((num_read != GNUTLS_E_AGAIN) && (num_read != GNUTLS_E_INTERRUPTED))) { - weechat_printf_date_tags ( - NULL, 0, "relay_client", - _("%s%s: reading data on socket for client %s%s%s: " - "error %d %s"), - weechat_prefix ("error"), RELAY_PLUGIN_NAME, - RELAY_COLOR_CHAT_CLIENT, - client->desc, - RELAY_COLOR_CHAT, - num_read, - (num_read == 0) ? _("(connection closed by peer)") : - gnutls_strerror (num_read)); + if (relay_config_display_clients[client->protocol]) + { + weechat_printf_date_tags ( + NULL, 0, "relay_client", + _("%s%s: reading data on socket for client %s%s%s: " + "error %d %s"), + weechat_prefix ("error"), RELAY_PLUGIN_NAME, + RELAY_COLOR_CHAT_CLIENT, + client->desc, + RELAY_COLOR_CHAT, + num_read, + (num_read == 0) ? _("(connection closed by peer)") : + gnutls_strerror (num_read)); + } relay_client_set_status (client, RELAY_STATUS_DISCONNECTED); } } @@ -717,17 +694,20 @@ relay_client_recv_cb (const void *pointer, void *data, int fd) if ((num_read == 0) || ((errno != EAGAIN) && (errno != EWOULDBLOCK))) { - weechat_printf_date_tags ( - NULL, 0, "relay_client", - _("%s%s: reading data on socket for client %s%s%s: " - "error %d %s"), - weechat_prefix ("error"), RELAY_PLUGIN_NAME, - RELAY_COLOR_CHAT_CLIENT, - client->desc, - RELAY_COLOR_CHAT, - errno, - (num_read == 0) ? _("(connection closed by peer)") : - strerror (errno)); + if (relay_config_display_clients[client->protocol]) + { + weechat_printf_date_tags ( + NULL, 0, "relay_client", + _("%s%s: reading data on socket for client %s%s%s: " + "error %d %s"), + weechat_prefix ("error"), RELAY_PLUGIN_NAME, + RELAY_COLOR_CHAT_CLIENT, + client->desc, + RELAY_COLOR_CHAT, + errno, + (num_read == 0) ? _("(connection closed by peer)") : + strerror (errno)); + } relay_client_set_status (client, RELAY_STATUS_DISCONNECTED); } } @@ -1067,7 +1047,8 @@ relay_client_send (struct t_relay_client *client, raw_size[1] = data_size; raw_flags[1] |= RELAY_RAW_FLAG_BINARY; if ((client->websocket == RELAY_CLIENT_WEBSOCKET_INITIALIZING) - || (client->send_data_type == RELAY_CLIENT_DATA_TEXT)) + || (client->send_data_type == RELAY_CLIENT_DATA_TEXT_LINE) + || (client->send_data_type == RELAY_CLIENT_DATA_TEXT_MULTILINE)) { raw_size[1]--; } @@ -1117,7 +1098,8 @@ relay_client_send (struct t_relay_client *client, opcode = WEBSOCKET_FRAME_OPCODE_CLOSE; break; default: - opcode = (client->send_data_type == RELAY_CLIENT_DATA_TEXT) ? + opcode = ((client->send_data_type == RELAY_CLIENT_DATA_TEXT_LINE) + || (client->send_data_type == RELAY_CLIENT_DATA_TEXT_MULTILINE)) ? WEBSOCKET_FRAME_OPCODE_TEXT : WEBSOCKET_FRAME_OPCODE_BINARY; break; } @@ -1325,7 +1307,7 @@ relay_client_new (int sock, const char *address, struct t_relay_server *server) new_client->hook_timer_handshake = NULL; new_client->gnutls_handshake_ok = 0; new_client->websocket = RELAY_CLIENT_WEBSOCKET_NOT_USED; - new_client->http_headers = NULL; + new_client->http_req = relay_http_request_alloc (); new_client->address = strdup ((address && address[0]) ? address : "local"); new_client->real_ip = NULL; @@ -1353,16 +1335,20 @@ relay_client_new (int sock, const char *address, struct t_relay_server *server) switch (new_client->protocol) { case RELAY_PROTOCOL_WEECHAT: - new_client->recv_data_type = RELAY_CLIENT_DATA_TEXT; + new_client->recv_data_type = RELAY_CLIENT_DATA_TEXT_LINE; new_client->send_data_type = RELAY_CLIENT_DATA_BINARY; break; case RELAY_PROTOCOL_IRC: - new_client->recv_data_type = RELAY_CLIENT_DATA_TEXT; - new_client->send_data_type = RELAY_CLIENT_DATA_TEXT; + new_client->recv_data_type = RELAY_CLIENT_DATA_TEXT_LINE; + new_client->send_data_type = RELAY_CLIENT_DATA_TEXT_LINE; + break; + case RELAY_PROTOCOL_API: + new_client->recv_data_type = RELAY_CLIENT_DATA_HTTP; + new_client->send_data_type = RELAY_CLIENT_DATA_HTTP; break; default: - new_client->recv_data_type = RELAY_CLIENT_DATA_TEXT; - new_client->send_data_type = RELAY_CLIENT_DATA_TEXT; + new_client->recv_data_type = RELAY_CLIENT_DATA_TEXT_MULTILINE; + new_client->send_data_type = RELAY_CLIENT_DATA_TEXT_MULTILINE; break; } new_client->partial_message = NULL; @@ -1439,6 +1425,16 @@ relay_client_new (int sock, const char *address, struct t_relay_server *server) relay_irc_get_initial_status (new_client); } break; + case RELAY_PROTOCOL_API: +#ifdef HAVE_CJSON + relay_api_alloc (new_client); + if (!new_client->tls) + { + new_client->status = + relay_api_get_initial_status (new_client); + } +#endif /* HAVE_CJSON */ + break; case RELAY_NUM_PROTOCOLS: break; } @@ -1454,29 +1450,32 @@ relay_client_new (int sock, const char *address, struct t_relay_server *server) last_relay_client = new_client; relay_clients = new_client; - if (server->unix_socket) + if (relay_config_display_clients[new_client->protocol]) { - weechat_printf_date_tags ( - NULL, 0, "relay_client", - _("%s: new client on path %s: %s%s%s (%s)"), - RELAY_PLUGIN_NAME, - server->path, - RELAY_COLOR_CHAT_CLIENT, - new_client->desc, - RELAY_COLOR_CHAT, - _(relay_client_status_string[new_client->status])); - } - else - { - weechat_printf_date_tags ( - NULL, 0, "relay_client", - _("%s: new client on port %s: %s%s%s (%s)"), - RELAY_PLUGIN_NAME, - server->path, - RELAY_COLOR_CHAT_CLIENT, - new_client->desc, - RELAY_COLOR_CHAT, - _(relay_client_status_string[new_client->status])); + if (server->unix_socket) + { + weechat_printf_date_tags ( + NULL, 0, "relay_client", + _("%s: new client on path %s: %s%s%s (%s)"), + RELAY_PLUGIN_NAME, + server->path, + RELAY_COLOR_CHAT_CLIENT, + new_client->desc, + RELAY_COLOR_CHAT, + _(relay_client_status_string[new_client->status])); + } + else + { + weechat_printf_date_tags ( + NULL, 0, "relay_client", + _("%s: new client on port %s: %s%s%s (%s)"), + RELAY_PLUGIN_NAME, + server->path, + RELAY_COLOR_CHAT_CLIENT, + new_client->desc, + RELAY_COLOR_CHAT, + _(relay_client_status_string[new_client->status])); + } } if (new_client->sock >= 0) @@ -1489,11 +1488,8 @@ relay_client_new (int sock, const char *address, struct t_relay_server *server) relay_client_count++; - if (!relay_buffer - && weechat_config_boolean (relay_config_look_auto_open_buffer)) - { + if (!relay_buffer && relay_config_auto_open_buffer[new_client->protocol]) relay_buffer_open (); - } relay_client_send_signal (new_client); @@ -1537,7 +1533,7 @@ relay_client_new_with_infolist (struct t_infolist *infolist) new_client->hook_timer_handshake = NULL; new_client->gnutls_handshake_ok = 0; new_client->websocket = weechat_infolist_integer (infolist, "websocket"); - new_client->http_headers = NULL; + new_client->http_req = relay_http_request_alloc (); new_client->address = strdup (weechat_infolist_string (infolist, "address")); str = weechat_infolist_string (infolist, "real_ip"); new_client->real_ip = (str) ? strdup (str) : NULL; @@ -1597,12 +1593,15 @@ relay_client_new_with_infolist (struct t_infolist *infolist) switch (new_client->protocol) { case RELAY_PROTOCOL_WEECHAT: - relay_weechat_alloc_with_infolist (new_client, - infolist); + relay_weechat_alloc_with_infolist (new_client, infolist); break; case RELAY_PROTOCOL_IRC: - relay_irc_alloc_with_infolist (new_client, - infolist); + relay_irc_alloc_with_infolist (new_client, infolist); + break; + case RELAY_PROTOCOL_API: +#ifdef HAVE_CJSON + relay_api_alloc_with_infolist (new_client, infolist); +#endif /* HAVE_CJSON */ break; case RELAY_NUM_PROTOCOLS: break; @@ -1644,7 +1643,8 @@ relay_client_set_status (struct t_relay_client *client, client->status = status; - if (client->status == RELAY_STATUS_CONNECTED) + if ((client->status == RELAY_STATUS_CONNECTED) + && relay_config_display_clients[client->protocol]) { weechat_printf_date_tags ( NULL, 0, "relay_client", @@ -1688,6 +1688,11 @@ relay_client_set_status (struct t_relay_client *client, case RELAY_PROTOCOL_IRC: relay_irc_close_connection (client); break; + case RELAY_PROTOCOL_API: +#ifdef HAVE_CJSON + relay_api_close_connection (client); +#endif /* HAVE_CJSON */ + break; case RELAY_NUM_PROTOCOLS: break; } @@ -1704,13 +1709,16 @@ relay_client_set_status (struct t_relay_client *client, RELAY_COLOR_CHAT); break; case RELAY_STATUS_DISCONNECTED: - weechat_printf_date_tags ( - NULL, 0, "relay_client", - _("%s: disconnected from client %s%s%s"), - RELAY_PLUGIN_NAME, - RELAY_COLOR_CHAT_CLIENT, - client->desc, - RELAY_COLOR_CHAT); + if (relay_config_display_clients[client->protocol]) + { + weechat_printf_date_tags ( + NULL, 0, "relay_client", + _("%s: disconnected from client %s%s%s"), + RELAY_PLUGIN_NAME, + RELAY_COLOR_CHAT_CLIENT, + client->desc, + RELAY_COLOR_CHAT); + } break; default: break; @@ -1772,8 +1780,7 @@ relay_client_free (struct t_relay_client *client) free (client->nonce); if (client->hook_timer_handshake) weechat_unhook (client->hook_timer_handshake); - if (client->http_headers) - weechat_hashtable_free (client->http_headers); + relay_http_request_free (client->http_req); if (client->hook_fd) weechat_unhook (client->hook_fd); if (client->hook_timer_send) @@ -1790,6 +1797,11 @@ relay_client_free (struct t_relay_client *client) case RELAY_PROTOCOL_IRC: relay_irc_free (client); break; + case RELAY_PROTOCOL_API: +#ifdef HAVE_CJSON + relay_api_free (client); +#endif /* HAVE_CJSON */ + break; case RELAY_NUM_PROTOCOLS: break; } @@ -1968,6 +1980,12 @@ relay_client_add_to_infolist (struct t_infolist *infolist, relay_irc_add_to_infolist (ptr_item, client, force_disconnected_state); break; + case RELAY_PROTOCOL_API: +#ifdef HAVE_CJSON + relay_api_add_to_infolist (ptr_item, client, + force_disconnected_state); +#endif /* HAVE_CJSON */ + break; case RELAY_NUM_PROTOCOLS: break; } @@ -1989,20 +2007,18 @@ relay_client_print_log () { weechat_log_printf (""); weechat_log_printf ("[relay client (addr:0x%lx)]", ptr_client); - weechat_log_printf (" id. . . . . . . . . . . . : %d", ptr_client->id); - weechat_log_printf (" desc. . . . . . . . . . . : '%s'", ptr_client->desc); - weechat_log_printf (" sock. . . . . . . . . . . : %d", ptr_client->sock); - weechat_log_printf (" server_port . . . . . . . : %d", ptr_client->server_port); - weechat_log_printf (" tls . . . . . . . . . . . : %d", ptr_client->tls); + weechat_log_printf (" id. . . . . . . . . . . . : %d", ptr_client->id); + weechat_log_printf (" desc. . . . . . . . . . . : '%s'", ptr_client->desc); + weechat_log_printf (" sock. . . . . . . . . . . : %d", ptr_client->sock); + weechat_log_printf (" server_port . . . . . . . : %d", ptr_client->server_port); + weechat_log_printf (" tls . . . . . . . . . . . : %d", ptr_client->tls); weechat_log_printf (" gnutls_sess . . . . . . . : 0x%lx", ptr_client->gnutls_sess); weechat_log_printf (" hook_timer_handshake. . . : 0x%lx", ptr_client->hook_timer_handshake); weechat_log_printf (" gnutls_handshake_ok . . . : 0x%lx", ptr_client->gnutls_handshake_ok); - weechat_log_printf (" websocket . . . . . . . . : %d", ptr_client->websocket); - weechat_log_printf (" http_headers. . . . . . . : 0x%lx (hashtable: '%s')", - ptr_client->http_headers, - weechat_hashtable_get_string (ptr_client->http_headers, "keys_values")); - weechat_log_printf (" address . . . . . . . . . : '%s'", ptr_client->address); - weechat_log_printf (" real_ip . . . . . . . . . : '%s'", ptr_client->real_ip); + weechat_log_printf (" websocket . . . . . . . . : %d", ptr_client->websocket); + relay_http_print_log (ptr_client->http_req); + weechat_log_printf (" address . . . . . . . . . : '%s'", ptr_client->address); + weechat_log_printf (" real_ip . . . . . . . . . : '%s'", ptr_client->real_ip); weechat_log_printf (" status. . . . . . . . . . : %d (%s)", ptr_client->status, relay_client_status_string[ptr_client->status]); @@ -2041,6 +2057,11 @@ relay_client_print_log () case RELAY_PROTOCOL_IRC: relay_irc_print_log (ptr_client); break; + case RELAY_PROTOCOL_API: +#ifdef HAVE_CJSON + relay_api_print_log (ptr_client); +#endif /* HAVE_CJSON */ + break; case RELAY_NUM_PROTOCOLS: break; } diff --git a/src/plugins/relay/relay-client.h b/src/plugins/relay/relay-client.h index 8803f519c..003fd47c6 100644 --- a/src/plugins/relay/relay-client.h +++ b/src/plugins/relay/relay-client.h @@ -25,6 +25,7 @@ #include struct t_relay_server; +struct t_relay_http_request; /* relay status */ @@ -43,8 +44,10 @@ enum t_relay_status enum t_relay_client_data_type { - RELAY_CLIENT_DATA_TEXT = 0, /* text messages */ + RELAY_CLIENT_DATA_TEXT_LINE = 0, /* text messages (on one line) */ RELAY_CLIENT_DATA_BINARY, /* binary messages */ + RELAY_CLIENT_DATA_HTTP, /* HTTP messages (mostly text) */ + RELAY_CLIENT_DATA_TEXT_MULTILINE, /* multi-line text (eg: JSON) */ /* number of data types */ RELAY_NUM_CLIENT_DATA_TYPES, }; @@ -53,7 +56,6 @@ enum t_relay_client_data_type enum t_relay_client_websocket_status { - /* 0=not a ws, 1=init ws, 2=ws ready */ RELAY_CLIENT_WEBSOCKET_NOT_USED = 0, /* no webseocket or not yet init. */ RELAY_CLIENT_WEBSOCKET_INITIALIZING, /* websocket used, initializing */ RELAY_CLIENT_WEBSOCKET_READY, /* websocket used, ready */ @@ -106,7 +108,7 @@ struct t_relay_client struct t_hook *hook_timer_handshake; /* timer for doing gnutls handshake*/ int gnutls_handshake_ok; /* 1 if handshake was done and OK */ enum t_relay_client_websocket_status websocket; /* websocket status */ - struct t_hashtable *http_headers; /* HTTP headers for websocket */ + struct t_relay_http_request *http_req; /* HTTP request */ char *address; /* string with IP address */ char *real_ip; /* real IP (X-Real-IP HTTP header) */ enum t_relay_status status; /* status (connecting, active,..) */ diff --git a/src/plugins/relay/relay-config.c b/src/plugins/relay/relay-config.c index 0016992e9..634868282 100644 --- a/src/plugins/relay/relay-config.c +++ b/src/plugins/relay/relay-config.c @@ -54,6 +54,7 @@ struct t_config_section *relay_config_section_path = NULL; /* relay config, look section */ struct t_config_option *relay_config_look_auto_open_buffer = NULL; +struct t_config_option *relay_config_look_display_clients = NULL; struct t_config_option *relay_config_look_raw_messages = NULL; /* relay config, color section */ @@ -101,12 +102,103 @@ struct t_config_option *relay_config_weechat_commands = NULL; /* other */ +int relay_config_auto_open_buffer[RELAY_NUM_PROTOCOLS]; +int relay_config_display_clients[RELAY_NUM_PROTOCOLS]; regex_t *relay_config_regex_allowed_ips = NULL; regex_t *relay_config_regex_websocket_allowed_origins = NULL; struct t_hashtable *relay_config_hashtable_irc_backlog_tags = NULL; char **relay_config_network_password_hash_algo_list = NULL; +/* + * Callback for changes on option "relay.look.auto_open_buffer". + */ + +void +relay_config_change_auto_open_buffer_cb (const void *pointer, void *data, + struct t_config_option *option) +{ + const char *auto_open_buffer; + const char *value_on = "irc,weechat", *value_off = ""; + char **items; + int i, num_items, protocol; + + /* make C compiler happy */ + (void) pointer; + (void) data; + (void) option; + + auto_open_buffer = weechat_config_string (relay_config_look_auto_open_buffer); + + /* old option was a boolean, use these values for compatibility */ + if (strcmp (auto_open_buffer, "on") == 0) + auto_open_buffer = value_on; + else if (strcmp (auto_open_buffer, "off") == 0) + auto_open_buffer = value_off; + + for (i = 0; i < RELAY_NUM_PROTOCOLS; i++) + { + relay_config_auto_open_buffer[i] = 0; + } + + if (auto_open_buffer[0]) + { + items = weechat_string_split (auto_open_buffer, ",", NULL, 0, 0, + &num_items); + if (items) + { + for (i = 0; i < num_items; i++) + { + protocol = relay_protocol_search (items[i]); + if (protocol >= 0) + relay_config_auto_open_buffer[protocol] = 1; + } + weechat_string_free_split (items); + } + } +} + +/* + * Callback for changes on option "relay.look.display_clients". + */ + +void +relay_config_change_display_clients_cb (const void *pointer, void *data, + struct t_config_option *option) +{ + const char *display_clients; + char **items; + int i, num_items, protocol; + + /* make C compiler happy */ + (void) pointer; + (void) data; + (void) option; + + display_clients = weechat_config_string (relay_config_look_display_clients); + + for (i = 0; i < RELAY_NUM_PROTOCOLS; i++) + { + relay_config_display_clients[i] = 0; + } + + if (display_clients[0]) + { + items = weechat_string_split (display_clients, ",", NULL, 0, 0, + &num_items); + if (items) + { + for (i = 0; i < num_items; i++) + { + protocol = relay_protocol_search (items[i]); + if (protocol >= 0) + relay_config_display_clients[protocol] = 1; + } + weechat_string_free_split (items); + } + } +} + /* * Callback for changes on options that require a refresh of relay list. */ @@ -1030,10 +1122,24 @@ relay_config_init () { relay_config_look_auto_open_buffer = weechat_config_new_option ( relay_config_file, relay_config_section_look, - "auto_open_buffer", "boolean", - N_("auto open relay buffer when a new client is connecting"), - NULL, 0, 0, "on", NULL, 0, - NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + "auto_open_buffer", "string", + N_("auto open relay buffer when a new client is connecting " + "using one of these protocols (comma-separated list); " + "allowed protocols: \"irc\", \"weechat\", \"api\""), + NULL, 0, 0, "irc,weechat", NULL, 0, + NULL, NULL, NULL, + &relay_config_change_auto_open_buffer_cb, NULL, NULL, + NULL, NULL, NULL); + relay_config_look_display_clients = weechat_config_new_option ( + relay_config_file, relay_config_section_look, + "display_clients", "string", + N_("display messages when clients connect/disconnect from relay " + "using one of these protocols (comma-separated list); " + "allowed protocols: \"irc\", \"weechat\", \"api\""), + NULL, 0, 0, "irc,weechat", NULL, 0, + NULL, NULL, NULL, + &relay_config_change_display_clients_cb, NULL, NULL, + NULL, NULL, NULL); relay_config_look_raw_messages = weechat_config_new_option ( relay_config_file, relay_config_section_look, "raw_messages", "integer", @@ -1185,11 +1291,11 @@ relay_config_init () relay_config_network_compression = weechat_config_new_option ( relay_config_file, relay_config_section_network, "compression", "integer", - N_("compression of messages sent to clients with \"weechat\" " - "protocol: 0 = disable compression, 1 = low compression / fast " - "... 100 = best compression / slow; the value is a percentage " - "converted to 1-9 for zlib and 1-19 for zstd; " - "the default value is recommended, it offers a good " + N_("compression of messages sent to clients with weechat and " + "api protocols: 0 = disable compression, " + "1 = low compression / fast ... 100 = best compression / slow; " + "the value is a percentage converted to 1-9 for zlib and 1-19 " + "for zstd; the default value is recommended, it offers a good " "compromise between compression and speed"), NULL, 0, 100, "20", NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); @@ -1279,8 +1385,8 @@ relay_config_init () "totp_secret", "string", N_("secret for the generation of the Time-based One-Time Password " "(TOTP), encoded in base32 (only letters and digits from 2 to 7); " - "it is used as second factor in weechat protocol, in addition to " - "the password, which must not be empty " + "it is used as second factor in weechat and api protocols, " + "in addition to the password, which must not be empty " "(empty value means no TOTP is required) " "(note: content is evaluated, see /help eval)"), NULL, 0, 0, "", NULL, 0, @@ -1435,6 +1541,8 @@ relay_config_read () rc = weechat_config_read (relay_config_file); if (rc == WEECHAT_CONFIG_READ_OK) { + relay_config_change_auto_open_buffer_cb (NULL, NULL, NULL); + relay_config_change_display_clients_cb (NULL, NULL, NULL); relay_config_change_network_allowed_ips (NULL, NULL, NULL); relay_config_change_network_password_hash_algo (NULL, NULL, NULL); relay_config_change_irc_backlog_tags (NULL, NULL, NULL); diff --git a/src/plugins/relay/relay-config.h b/src/plugins/relay/relay-config.h index 3570c4909..bc49aca01 100644 --- a/src/plugins/relay/relay-config.h +++ b/src/plugins/relay/relay-config.h @@ -67,6 +67,8 @@ extern struct t_config_option *relay_config_irc_backlog_time_format; extern struct t_config_option *relay_config_weechat_commands; +extern int relay_config_auto_open_buffer[]; +extern int relay_config_display_clients[]; extern regex_t *relay_config_regex_allowed_ips; extern regex_t *relay_config_regex_websocket_allowed_origins; extern struct t_hashtable *relay_config_hashtable_irc_backlog_tags; diff --git a/src/plugins/relay/relay-http.c b/src/plugins/relay/relay-http.c new file mode 100644 index 000000000..d0bf015ff --- /dev/null +++ b/src/plugins/relay/relay-http.c @@ -0,0 +1,1284 @@ +/* + * relay-http.c - HTTP request parser for relay plugin + * + * Copyright (C) 2023-2024 Sébastien Helleu + * + * This file is part of WeeChat, the extensible chat client. + * + * WeeChat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * WeeChat is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with WeeChat. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#ifdef HAVE_ZSTD +#include +#endif + +#include "../weechat-plugin.h" +#include "relay.h" +#include "relay-client.h" +#include "relay-config.h" +#include "relay-http.h" +#include "relay-raw.h" +#include "relay-websocket.h" +#ifdef HAVE_CJSON +#include "api/relay-api.h" +#endif + +#define HEX2DEC(c) (((c >= 'a') && (c <= 'f')) ? c - 'a' + 10 : \ + ((c >= 'A') && (c <= 'F')) ? c - 'A' + 10 : \ + c - '0') + + +/* + * Reinitializes the HTTP request status. + */ + +void +relay_http_request_reinit (struct t_relay_http_request *request) +{ + request->status = RELAY_HTTP_METHOD; + weechat_string_dyn_copy (request->raw, NULL); + if (request->method) + { + free (request->method); + request->method = NULL; + } + if (request->path) + { + free (request->path); + request->path = NULL; + } + if (request->path_items) + { + weechat_string_free_split (request->path_items); + request->path_items = NULL; + } + request->num_path_items = 0; + weechat_hashtable_remove_all (request->params); + if (request->http_version) + { + free (request->http_version); + request->http_version = NULL; + } + weechat_hashtable_remove_all (request->headers); + weechat_hashtable_remove_all (request->accept_encoding); + request->content_length = 0; + request->body_size = 0; + if (request->body) + { + free (request->body); + request->body = NULL; + } +} + +/* + * Allocates a t_relay_http_request structure. + */ + +struct t_relay_http_request * +relay_http_request_alloc () +{ + struct t_relay_http_request *new_request; + + new_request = (struct t_relay_http_request *)malloc (sizeof (*new_request)); + if (!new_request) + return NULL; + + new_request->status = RELAY_HTTP_METHOD; + new_request->raw = weechat_string_dyn_alloc (64); + new_request->method = NULL; + new_request->path = NULL; + new_request->path_items = NULL; + new_request->num_path_items = 0; + new_request->params = weechat_hashtable_new ( + 32, + WEECHAT_HASHTABLE_STRING, + WEECHAT_HASHTABLE_STRING, + NULL, NULL); + new_request->http_version = NULL; + new_request->headers = weechat_hashtable_new ( + 32, + WEECHAT_HASHTABLE_STRING, + WEECHAT_HASHTABLE_STRING, + NULL, NULL); + new_request->accept_encoding = weechat_hashtable_new ( + 32, + WEECHAT_HASHTABLE_STRING, + WEECHAT_HASHTABLE_STRING, + NULL, NULL); + new_request->content_length = 0; + new_request->body_size = 0; + new_request->body = NULL; + + return new_request; +} + +/* + * Decode URL: replace "%" chars by their values (eg: "%23" -> "#"). + */ + +char * +relay_http_url_decode (const char *url) +{ + const char *ptr_url, *ptr_next; + char **out, str_char[2]; + int length; + + if (!url) + return NULL; + + length = strlen (url); + out = weechat_string_dyn_alloc ((length > 0) ? length : 1); + if (!out) + return NULL; + + ptr_url = url; + while (ptr_url && ptr_url[0]) + { + if ((ptr_url[0] == '%') + && (isxdigit ((unsigned char)ptr_url[1])) + && (isxdigit ((unsigned char)ptr_url[2]))) + { + snprintf (str_char, sizeof (str_char), + "%c", + (HEX2DEC(ptr_url[1]) << 4) + HEX2DEC(ptr_url[2])); + weechat_string_dyn_concat (out, str_char, -1); + ptr_url += 3; + } + else + { + ptr_next = weechat_utf8_next_char (ptr_url); + weechat_string_dyn_concat ( + out, + ptr_url, + (ptr_next) ? ptr_next - ptr_url : -1); + ptr_url = ptr_next; + } + } + + return weechat_string_dyn_free (out, 0); +} + +/* + * Get decoded path items from path. + */ + +void +relay_http_parse_path (const char *path, + char ***paths, int *num_paths, + struct t_hashtable *params) +{ + char *pos, *str_path, *str_params, **items_path, **items2_path; + char **items_params, *name, *value; + int i, num_items_path, num_items_params; + + *paths = NULL; + *num_paths = 0; + + if (!path) + return; + + str_path = NULL; + str_params = NULL; + items_path = NULL; + items2_path = NULL; + + pos = strchr (path, '?'); + if (pos) + { + str_path = weechat_strndup (path, pos - path); + str_params = strdup (pos + 1); + } + else + { + str_path = strdup (path); + } + + /* + * decode path items (until '?' or end of string): + * "/path/to/irc.libera.%23weechat" + * => ["path", "to", "irc.libera.#weechat"] + */ + items_path = weechat_string_split ( + (str_path[0] == '/') ? str_path + 1 : str_path, + "/", + NULL, + WEECHAT_STRING_SPLIT_STRIP_LEFT + | WEECHAT_STRING_SPLIT_STRIP_RIGHT + | WEECHAT_STRING_SPLIT_COLLAPSE_SEPS, + 0, &num_items_path); + if (items_path && (num_items_path > 0)) + { + items2_path = malloc (sizeof (*items_path) * (num_items_path + 1)); + if (items2_path) + { + for (i = 0; i < num_items_path; i++) + { + items2_path[i] = relay_http_url_decode (items_path[i]); + } + items2_path[num_items_path] = NULL; + } + *paths = items2_path; + *num_paths = num_items_path; + } + + /* + * decode parameters (starting after '?'): + * "/path/to/irc.libera.%23weechat?option=2&bool=off&fields=a,b,c" + * => {"option": "2", "bool": "off", "fields": "a,b,c"} + */ + if (str_params) + { + items_params = weechat_string_split (str_params, "&", NULL, 0, 0, + &num_items_params); + if (items_params && (num_items_params > 0)) + { + for (i = 0; i < num_items_params; i++) + { + pos = strchr (items_params[i], '='); + if (pos) + { + name = weechat_strndup (items_params[i], pos - items_params[i]); + value = relay_http_url_decode (pos + 1); + } + else + { + name = strdup (items_params[i]); + value = strdup (""); + } + if (params) + weechat_hashtable_set (params, name, value); + if (name) + free (name); + if (value) + free (value); + } + } + } + + if (str_path) + free (str_path); + if (str_params) + free (str_params); + if (items_path) + weechat_string_free_split (items_path); +} + +/* + * Parses and saves method and path. + * + * Returns: + * 1: OK, method and path saved + * 0: error: invalid format + */ + +int +relay_http_parse_method_path (struct t_relay_http_request *request, + const char *method_path) +{ + char **items; + int num_items; + + if (!method_path || !method_path[0]) + return 0; + + weechat_string_dyn_concat (request->raw, method_path, -1); + weechat_string_dyn_concat (request->raw, "\n", -1); + + items = weechat_string_split (method_path, " ", NULL, + WEECHAT_STRING_SPLIT_STRIP_LEFT + | WEECHAT_STRING_SPLIT_STRIP_RIGHT + | WEECHAT_STRING_SPLIT_COLLAPSE_SEPS, + 0, &num_items); + if (!items || (num_items < 2)) + goto error; + + if (request->method) + free (request->method); + request->method = strdup (items[0]); + + if (request->path) + free (request->path); + request->path = strdup (items[1]); + + if (num_items > 2) + { + if (request->http_version) + free (request->http_version); + request->http_version = strdup (items[2]); + } + + relay_http_parse_path (request->path, + &(request->path_items), + &(request->num_path_items), + request->params); + + request->status = RELAY_HTTP_HEADERS; + + weechat_string_free_split (items); + + return 1; + +error: + if (items) + weechat_string_free_split (items); + request->status = RELAY_HTTP_END; + return 0; +} + +/* + * Parses and saves a HTTP header in hashtable "headers". + * + * Returns: + * 1: OK, header saved + * 0: error: invalid format + */ + +int +relay_http_parse_header (struct t_relay_http_request *request, + const char *header) +{ + char *pos, *name, *name_lower, *error, **items; + const char *ptr_value; + int i, num_items; + long number; + + weechat_string_dyn_concat (request->raw, header, -1); + weechat_string_dyn_concat (request->raw, "\n", -1); + + /* empty line => end of headers */ + if (!header || !header[0]) + { + request->status = (request->content_length > 0) ? + RELAY_HTTP_BODY : RELAY_HTTP_END; + return 1; + } + + pos = strchr (header, ':'); + + /* not a valid header */ + if (!pos || (pos == header)) + return 0; + + /* get header name, which is case-insensitive */ + name = weechat_strndup (header, pos - header); + if (!name) + return 0; + name_lower = weechat_string_tolower (name); + if (!name_lower) + { + free (name); + return 0; + } + + /* get pointer on header value */ + ptr_value = pos + 1; + while (ptr_value[0] == ' ') + { + ptr_value++; + } + + /* add header in the hashtable */ + weechat_hashtable_set (request->headers, name_lower, ptr_value); + + /* if header is "Accept-Encoding", save the allowed encoding */ + if (strcmp (name_lower, "accept-encoding") == 0) + { + items = weechat_string_split (ptr_value, ",", NULL, 0, 0, &num_items); + if (items) + { + for (i = 0; i < num_items; i++) + { + pos = items[i]; + while (pos[0] == ' ') + { + pos++; + } + weechat_hashtable_set (request->accept_encoding, + pos, NULL); + } + weechat_string_free_split (items); + } + } + + /* if header is "Content-Length", save the length */ + if (strcmp (name_lower, "content-length") == 0) + { + error = NULL; + number = strtol (ptr_value, &error, 10); + if (error && !error[0]) + request->content_length = (int)number; + } + + free (name); + free (name_lower); + + return 1; +} + +/* + * Adds bytes to HTTP body, changes the status to RELAY_HTTP_END if the body + * is complete. + */ + +void +relay_http_add_to_body (struct t_relay_http_request *request, + char **partial_message) +{ + char *new_body, *new_partial; + int num_bytes_missing, length_msg, length; + + if (!partial_message || !*partial_message) + return; + + num_bytes_missing = request->content_length + - request->body_size; + if (num_bytes_missing <= 0) + { + request->status = RELAY_HTTP_END; + return; + } + + length_msg = strlen (*partial_message); + if (num_bytes_missing >= length_msg) + { + new_body = realloc ( + request->body, + request->body_size + length_msg + 1); + if (new_body) + { + request->body = new_body; + memcpy (request->body + request->body_size, + *partial_message, + length_msg); + request->body[request->body_size + length_msg] = '\0'; + request->body_size += length_msg; + weechat_string_dyn_concat (request->raw, *partial_message, -1); + } + free (*partial_message); + *partial_message = NULL; + } + else + { + new_body = realloc ( + request->body, + request->body_size + num_bytes_missing + 1); + if (new_body) + { + request->body = new_body; + memcpy (request->body + request->body_size, + *partial_message, + num_bytes_missing); + request->body[request->body_size + num_bytes_missing] = '\0'; + request->body_size += num_bytes_missing; + weechat_string_dyn_concat (request->raw, + *partial_message, num_bytes_missing); + length = strlen (*partial_message + num_bytes_missing); + new_partial = malloc (length + 1); + if (new_partial) + { + memcpy (new_partial, + *partial_message + num_bytes_missing, + length + 1); + free (*partial_message); + *partial_message = new_partial; + } + } + } + + if (request->body_size >= request->content_length) + request->status = RELAY_HTTP_END; +} + +/* + * Checks if authentication is OK. + * + * Returns: + * 0: authentication OK (password + TOTP if enabled) + * -1: missing password + * -2: invalid password + * -3: missing TOTP + * -4: invalid TOTP + * -5: out of memory + */ + +int +relay_http_check_auth (struct t_relay_http_request *request) +{ + const char *auth, *client_totp, *pos; + char *relay_password, *totp_secret, *info_totp_args, *info_totp; + char *user_pass; + int rc, length, totp_ok; + + rc = 0; + relay_password = NULL; + totp_secret = NULL; + user_pass = NULL; + + auth = weechat_hashtable_get (request->headers, "authorization"); + if (!auth || (weechat_strncasecmp (auth, "basic ", 6) != 0)) + { + rc = -1; + goto end; + } + + pos = auth + 6; + while (pos[0] == ' ') + { + pos++; + } + + length = strlen (pos); + user_pass = malloc (length + 1); + if (!user_pass) + { + rc = -5; + goto end; + } + length = weechat_string_base_decode (64, pos, user_pass); + if (length < 0) + { + rc = -2; + goto end; + } + if (strncmp (user_pass, "weechat:", 8) != 0) + { + rc = -2; + goto end; + } + + relay_password = weechat_string_eval_expression ( + weechat_config_string (relay_config_network_password), + NULL, NULL, NULL); + + if (!relay_password) + { + rc = -5; + goto end; + } + + if (strcmp (user_pass + 8, relay_password) != 0) + { + rc = -2; + goto end; + } + + totp_secret = weechat_string_eval_expression ( + weechat_config_string (relay_config_network_totp_secret), + NULL, NULL, NULL); + if (totp_secret && totp_secret[0]) + { + client_totp = weechat_hashtable_get (request->headers, "x-weechat-totp"); + if (!client_totp || !client_totp[0]) + { + rc = -3; + goto end; + } + length = strlen (totp_secret) + strlen (client_totp) + 16 + 1; + info_totp_args = malloc (length); + if (info_totp_args) + { + /* validate the TOTP received from the client */ + snprintf (info_totp_args, length, + "%s,%s,0,%d", + totp_secret, /* the shared secret */ + client_totp, /* the TOTP from client */ + weechat_config_integer (relay_config_network_totp_window)); + info_totp = weechat_info_get ("totp_validate", info_totp_args); + totp_ok = (info_totp && (strcmp (info_totp, "1") == 0)) ? + 1 : 0; + if (info_totp) + free (info_totp); + free (info_totp_args); + if (!totp_ok) + { + rc = -4; + goto end; + } + } + } + +end: + if (relay_password) + free (relay_password); + if (totp_secret) + free (totp_secret); + if (user_pass) + free (user_pass); + return rc; +} + +/* + * Processes HTTP websocket request. + */ + +void +relay_http_process_websocket (struct t_relay_client *client) +{ + const char *ptr_real_ip; + char *handshake; + int rc; + + rc = relay_websocket_client_handshake_valid (client); + + if (rc == -1) + { + relay_http_send (client, RELAY_HTTP_400_BAD_REQUEST, NULL, NULL, 0); + if (weechat_relay_plugin->debug >= 1) + { + weechat_printf_date_tags ( + NULL, 0, "relay_client", + _("%s%s: invalid websocket handshake received for client %s%s%s"), + weechat_prefix ("error"), + RELAY_PLUGIN_NAME, + RELAY_COLOR_CHAT_CLIENT, + client->desc, + RELAY_COLOR_CHAT); + } + relay_client_set_status (client, RELAY_STATUS_DISCONNECTED); + return; + } + + if (rc == -2) + { + relay_http_send (client, RELAY_HTTP_403_FORBIDDEN, NULL, NULL, 0); + if (weechat_relay_plugin->debug >= 1) + { + weechat_printf_date_tags ( + NULL, 0, "relay_client", + _("%s%s: origin \"%s\" is not allowed for websocket"), + weechat_prefix ("error"), + RELAY_PLUGIN_NAME, + weechat_hashtable_get (client->http_req->headers, "origin")); + } + relay_client_set_status (client, RELAY_STATUS_DISCONNECTED); + return; + } + + /* handshake from client is valid, auth is mandatory for "api" protocol */ + if (client->protocol == RELAY_PROTOCOL_API) + { + switch (relay_http_check_auth (client->http_req)) + { + case 0: /* authentication OK */ + relay_client_set_status (client, RELAY_STATUS_CONNECTED); + break; + case -1: /* missing password */ + relay_http_send_error_json (client, RELAY_HTTP_401_UNAUTHORIZED, + NULL, + RELAY_HTTP_ERROR_MISSING_PASSWORD); + relay_client_set_status (client, RELAY_STATUS_AUTH_FAILED); + return; + case -2: /* invalid password */ + relay_http_send_error_json (client, RELAY_HTTP_401_UNAUTHORIZED, + NULL, + RELAY_HTTP_ERROR_INVALID_PASSWORD); + relay_client_set_status (client, RELAY_STATUS_AUTH_FAILED); + return; + case -3: /* missing TOTP */ + relay_http_send_error_json (client, RELAY_HTTP_401_UNAUTHORIZED, + NULL, + RELAY_HTTP_ERROR_MISSING_TOTP); + relay_client_set_status (client, RELAY_STATUS_AUTH_FAILED); + return; + case -4: /* invalid TOTP */ + relay_http_send_error_json (client, RELAY_HTTP_401_UNAUTHORIZED, + NULL, + RELAY_HTTP_ERROR_INVALID_TOTP); + relay_client_set_status (client, RELAY_STATUS_AUTH_FAILED); + return; + case -5: /* out of memory */ + relay_http_send_error_json (client, RELAY_HTTP_401_UNAUTHORIZED, + NULL, + RELAY_HTTP_ERROR_OUT_OF_MEMORY); + relay_client_set_status (client, RELAY_STATUS_AUTH_FAILED); + return; + } + } + + handshake = relay_websocket_build_handshake (client); + if (handshake) + { + relay_client_send (client, + RELAY_CLIENT_MSG_STANDARD, + handshake, + strlen (handshake), NULL); + free (handshake); + client->websocket = RELAY_CLIENT_WEBSOCKET_READY; + if (client->protocol == RELAY_PROTOCOL_API) + { + /* "api" protocol uses JSON in input/output (multi-line text) */ + client->recv_data_type = RELAY_CLIENT_DATA_TEXT_MULTILINE; + client->send_data_type = RELAY_CLIENT_DATA_TEXT_MULTILINE; + } + } + + ptr_real_ip = weechat_hashtable_get ( + client->http_req->headers, "x-real-ip"); + if (ptr_real_ip) + { + if (client->real_ip) + free (client->real_ip); + client->real_ip = strdup (ptr_real_ip); + relay_client_set_desc (client); + weechat_printf_date_tags ( + NULL, 0, "relay_client", + _("%s: websocket client %s%s%s has real IP " + "address \"%s\""), + RELAY_PLUGIN_NAME, + RELAY_COLOR_CHAT_CLIENT, + client->desc, + RELAY_COLOR_CHAT, + ptr_real_ip); + } +} + +/* + * Processes HTTP request. + */ + +void +relay_http_process_request (struct t_relay_client *client) +{ + if (client->http_req->raw) + { + relay_raw_print (client, RELAY_CLIENT_MSG_STANDARD, + RELAY_RAW_FLAG_RECV, + *(client->http_req->raw), + strlen (*(client->http_req->raw)) + 1); + } + + /* if websocket is initializing */ + if (client->websocket == RELAY_CLIENT_WEBSOCKET_INITIALIZING) + { + relay_http_process_websocket (client); + } + else + { +#ifdef HAVE_CJSON + if (client->protocol == RELAY_PROTOCOL_API) + relay_api_recv_http (client, client->http_req); +#endif /* HAVE_CJSON */ + } +} + +/* + * Reads HTTP data from a client. + */ + +void +relay_http_recv (struct t_relay_client *client, const char *data) +{ + char *new_partial, *pos; + int length; + + if (client->partial_message) + { + new_partial = realloc (client->partial_message, + strlen (client->partial_message) + + strlen (data) + 1); + if (!new_partial) + return; + client->partial_message = new_partial; + strcat (client->partial_message, data); + } + else + { + client->partial_message = strdup (data); + } + + while (client->partial_message) + { + if ((client->http_req->status == RELAY_HTTP_METHOD) + || (client->http_req->status == RELAY_HTTP_HEADERS)) + { + pos = strchr (client->partial_message, '\r'); + if (!pos) + break; + pos[0] = '\0'; + if (client->http_req->status == RELAY_HTTP_METHOD) + { + relay_http_parse_method_path (client->http_req, + client->partial_message); + } + else + { + relay_http_parse_header (client->http_req, + client->partial_message); + } + pos[0] = '\r'; + pos++; + if (pos[0] == '\n') + pos++; + length = strlen (pos); + if (length > 0) + { + new_partial = malloc (length + 1); + if (new_partial) + { + memcpy (new_partial, pos, length + 1); + free (client->partial_message); + client->partial_message = new_partial; + } + } + else + { + free (client->partial_message); + client->partial_message = NULL; + } + } + else if (client->http_req->status == RELAY_HTTP_BODY) + { + relay_http_add_to_body (client->http_req, &(client->partial_message)); + } + + /* process the request if it's ready to be processed (all parsed) */ + if (client->http_req->status == RELAY_HTTP_END) + { + relay_http_process_request (client); + relay_http_request_reinit (client->http_req); + } + + /* + * we continue to process HTTP requests only if websocket is + * initializing or for "api" relay + */ + if ((client->websocket != RELAY_CLIENT_WEBSOCKET_INITIALIZING) + && (client->protocol != RELAY_PROTOCOL_API)) + { + break; + } + } +} + +/* + * Compresses body of HTTP request with gzip or zstd, if all conditions are met: + * - body is not empty + * - gzip or ztsd is allowed by client (header "Accept-Encoding") + * (for zstd, WeeChat must be compiled with zstd support) + * - compression is enabled (option relay.network.compression > 0) + * + * "compressed_size" is set to the compressed size (0 if error). + * + * "http_content_encoding" is a pointer to a string that will be set with + * the HTTP header "Content-Encoding", if the compression is successful + * (for example: "Content-Encoding: gzip"). + * + * Returns pointer to compressed data or NULL if error. + * + * Note: result must be freed after use. + */ + +char * +relay_http_compress (struct t_relay_http_request *request, + const char *data, int data_size, + int *compressed_size, + char *http_content_encoding, + int http_content_encoding_size) +{ + int rc, compression, compression_level, comp_deflate, comp_gzip, comp_zstd; + Bytef *dest; + uLongf dest_size; + z_stream strm; +#ifdef HAVE_ZSTD + size_t zstd_comp_size; +#endif + + if (compressed_size) + *compressed_size = 0; + if (http_content_encoding) + http_content_encoding[0] = '\0'; + + if (!data || (data_size <= 0) || !compressed_size + || !http_content_encoding || (http_content_encoding_size <= 0)) + { + return NULL; + } + + compression = weechat_config_integer (relay_config_network_compression); + if (compression <= 0) + return NULL; + + /* + * compression used by priority if allowed: + * 1. zstd + * 2. deflate + * 3. gzip + */ + comp_deflate = weechat_hashtable_has_key (request->accept_encoding, "deflate"); + comp_gzip = weechat_hashtable_has_key (request->accept_encoding, "gzip"); +#ifdef HAVE_ZSTD + comp_zstd = weechat_hashtable_has_key (request->accept_encoding, "zstd"); +#else + comp_zstd = 0; +#endif /* HAVE_ZSTD */ + + if (!comp_deflate && !comp_gzip && !comp_zstd) + return NULL; + + if (comp_deflate) + comp_gzip = 0; + + dest = NULL; + dest_size = 0; + +#ifdef HAVE_ZSTD + /* compress with zstd */ + if (!dest && comp_zstd) + { + /* convert % to zstd compression level (1-19) */ + compression_level = (((compression - 1) * 19) / 100) + 1; + dest_size = ZSTD_compressBound (data_size); + dest = malloc (dest_size); + if (dest) + { + zstd_comp_size = ZSTD_compress( + dest, + dest_size, + (void *)data, + data_size, + compression_level); + if (zstd_comp_size > 0) + { + *compressed_size = zstd_comp_size; + strcat (http_content_encoding, "Content-Encoding: zstd\r\n"); + } + else + { + free (dest); + dest = NULL; + } + } + } +#endif /* HAVE_ZSTD */ + + /* compress with deflate (zlib) or gzip */ + if (!dest && (comp_deflate || comp_gzip)) + { + /* convert % to zlib compression level (1-9) */ + compression_level = (((compression - 1) * 9) / 100) + 1; + dest_size = compressBound (data_size); + dest = malloc (dest_size); + if (dest) + { + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.avail_in = (uInt)data_size; + strm.next_in = (Bytef *)data; + strm.avail_out = (uInt)dest_size; + strm.next_out = (Bytef *)dest; + rc = deflateInit2 ( + &strm, + compression_level, + Z_DEFLATED, /* method */ + (comp_gzip) ? 15 + 16 : 15, /* + 16 = gzip instead of zlib */ + 8, /* memLevel */ + Z_DEFAULT_STRATEGY); /* strategy */ + if (rc == Z_OK) + { + rc = deflate (&strm, Z_FINISH); + (void) deflateEnd (&strm); + if ((rc == Z_STREAM_END) || (rc == Z_OK)) + { + *compressed_size = strm.total_out; + if (comp_deflate) + strcat (http_content_encoding, "Content-Encoding: deflate\r\n"); + else if (comp_gzip) + strcat (http_content_encoding, "Content-Encoding: gzip\r\n"); + } + else + { + free (dest); + dest = NULL; + } + } + else + { + free (dest); + dest = NULL; + } + } + } + + return (char *)dest; +} + +/* + * Sends a HTTP message to client. + * + * Argument "http" is a HTTP code + message, for example: + * "403 Forbidden". + * + * Returns number of bytes sent to client, -1 if error. + */ + +int +relay_http_send (struct t_relay_client *client, + int return_code, const char *message, + const char *headers, + const char *body, int body_size) +{ + const char *ptr_body; + char str_header[1024], str_content_encoding[256]; + char *compressed_body, *http_message, *raw_message; + int length_header, length_msg, length_raw, num_bytes; + int *ptr_body_size, compressed_body_size; + + if (!client || !message || (body_size < 0)) + return -1; + + str_content_encoding[0] = '\0'; + + ptr_body = body; + ptr_body_size = &body_size; + + compressed_body = relay_http_compress (client->http_req, body, body_size, + &compressed_body_size, + str_content_encoding, + sizeof (str_content_encoding)); + if (compressed_body) + { + ptr_body = compressed_body; + ptr_body_size = &compressed_body_size; + } + + snprintf (str_header, sizeof (str_header), + "HTTP/1.1 %d %s\r\n" + "%s%s" + "%s" + "Content-Length: %d\r\n" + "\r\n", + return_code, + (message) ? message : "", + (headers) ? headers : "", + (headers && headers[0]) ? "\r\n" : "", + str_content_encoding, + *ptr_body_size); + + length_header = strlen (str_header); + + if (!ptr_body || (*ptr_body_size <= 0)) + { + num_bytes = relay_client_send (client, RELAY_CLIENT_MSG_STANDARD, + str_header, length_header, NULL); + } + else + { + length_msg = length_header + (*ptr_body_size); + http_message = malloc (length_msg + 1); + if (http_message) + { + memcpy (http_message, str_header, length_header); + memcpy (http_message + length_header, ptr_body, *ptr_body_size); + http_message[length_msg] = '\0'; + if (compressed_body) + { + length_raw = strlen (str_header) + 64; + raw_message = malloc (length_raw); + if (raw_message) + { + snprintf (raw_message, length_raw, + "%s[%d bytes data]", + str_header, + *ptr_body_size); + } + } + else + { + raw_message = NULL; + } + num_bytes = relay_client_send (client, RELAY_CLIENT_MSG_STANDARD, + http_message, length_msg, + raw_message); + if (raw_message) + free (raw_message); + free (http_message); + } + else + { + num_bytes = -1; + } + } + + if (compressed_body) + free (compressed_body); + + return num_bytes; +} + +/* + * Sends JSON string to client. + * + * Returns number of bytes sent to client, -1 if error. + */ + +int +relay_http_send_json (struct t_relay_client *client, + int return_code, + const char *message, + const char *headers, + const char *json_string) +{ + int num_bytes, length; + char *headers2; + + if (!client || !message || !json_string) + return -1; + + num_bytes = -1; + headers2 = NULL; + + length = 128 + ((headers) ? strlen (headers) : 0); + headers2 = malloc (length); + if (headers2) + { + snprintf (headers2, length, + "%s%s%s", + (headers) ? headers : "", + (headers && headers[0]) ? "\r\n" : "", + "Content-Type: application/json; charset=utf-8"); + } + + num_bytes = relay_http_send (client, return_code, message, headers2, + json_string, strlen (json_string)); + + if (headers2) + free (headers2); + + return num_bytes; +} + +/* + * Sends JSON error to client. + * + * Returns number of bytes sent to client, -1 if error. + */ + +int +relay_http_send_error_json (struct t_relay_client *client, + int return_code, + const char *message, + const char *headers, + const char *format, ...) +{ + int num_bytes, length; + char *error_msg, *json; + + if (!client || !message || !format) + return -1; + + weechat_va_format (format); + if (!vbuffer) + return -1; + + num_bytes = -1; + error_msg = NULL; + json = NULL; + + error_msg = weechat_string_replace (vbuffer, "\"", "\\\""); + if (!error_msg) + goto end; + + length = strlen (error_msg) + 64; + json = malloc (length); + if (!json) + goto end; + snprintf (json, length, "{\"error\": \"%s\"}", error_msg); + + num_bytes = relay_http_send_json (client, return_code, message, headers, + json); + +end: + free (vbuffer); + if (error_msg) + free (error_msg); + if (json) + free (json); + return num_bytes; +} + +/* + * Frees a HTTP request. + */ + +void +relay_http_request_free (struct t_relay_http_request *request) +{ + if (request->raw) + weechat_string_dyn_free (request->raw, 1); + if (request->method) + free (request->method); + if (request->path) + free (request->path); + if (request->path_items) + weechat_string_free_split (request->path_items); + if (request->params) + weechat_hashtable_free (request->params); + if (request->http_version) + free (request->http_version); + if (request->headers) + weechat_hashtable_free (request->headers); + if (request->accept_encoding) + weechat_hashtable_free (request->accept_encoding); + if (request->body) + free (request->body); + + free (request); +} + +/* + * Prints HTTP request in WeeChat log file (usually for crash dump). + */ + +void +relay_http_print_log (struct t_relay_http_request *request) +{ + int i; + + weechat_log_printf (" [http_request]"); + weechat_log_printf (" raw . . . . . . . . . . : '%s'", + (request->raw) ? *(request->raw) : NULL); + weechat_log_printf (" status. . . . . . . . . : %d", request->status); + weechat_log_printf (" method. . . . . . . . . : '%s'", request->method); + weechat_log_printf (" path. . . . . . . . . . : '%s'", request->path); + weechat_log_printf (" path_items. . . . . . . : 0x%lx", request->path_items); + if (request->path_items) + { + for (i = 0; request->path_items[0]; i++) + { + weechat_log_printf (" '%s'", request->path_items[i]); + } + } + weechat_log_printf (" num_path_items. . . . . : %d", request->num_path_items); + weechat_log_printf (" params. . . . . . . . . : 0x%lx (hashtable: '%s')", + request->params, + weechat_hashtable_get_string (request->params, "keys_values")); + weechat_log_printf (" http_version. . . . . . : '%s'", request->http_version); + weechat_log_printf (" headers . . . . . . . . : 0x%lx (hashtable: '%s')", + request->headers, + weechat_hashtable_get_string (request->headers, "keys_values")); + weechat_log_printf (" accept_encoding . . . . : 0x%lx (hashtable: '%s')", + request->accept_encoding, + weechat_hashtable_get_string (request->accept_encoding, + "keys_values")); + weechat_log_printf (" content_length. . . . . : %d", request->content_length); + weechat_log_printf (" body_size . . . . . . . : %d", request->body_size); + weechat_log_printf (" body. . . . . . . . . . : '%s'", request->body); +} diff --git a/src/plugins/relay/relay-http.h b/src/plugins/relay/relay-http.h new file mode 100644 index 000000000..231916ebd --- /dev/null +++ b/src/plugins/relay/relay-http.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2023-2024 Sébastien Helleu + * + * This file is part of WeeChat, the extensible chat client. + * + * WeeChat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * WeeChat is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with WeeChat. If not, see . + */ + +#ifndef WEECHAT_PLUGIN_RELAY_HTTP_H +#define WEECHAT_PLUGIN_RELAY_HTTP_H + +struct t_relay_client; + +enum t_relay_client_http_status +{ + RELAY_HTTP_METHOD = 0, /* reading method (eg: GET, POST) */ + RELAY_HTTP_HEADERS, /* reading headers */ + RELAY_HTTP_BODY, /* reading body */ + RELAY_HTTP_END, /* end of HTTP request */ + /* number of HTTP status */ + RELAY_NUM_HTTP_STATUS, +}; + +#define RELAY_HTTP_200_OK 200, "OK" +#define RELAY_HTTP_204_NO_CONTENT 204, "No Content" +#define RELAY_HTTP_400_BAD_REQUEST 400, "Bad Request" +#define RELAY_HTTP_401_UNAUTHORIZED 401, "Unauthorized" +#define RELAY_HTTP_403_FORBIDDEN 403, "Forbidden" +#define RELAY_HTTP_404_NOT_FOUND 404, "Not Found" +#define RELAY_HTTP_500_INTERNAL_SERVER_ERROR 500, "Internal Server Error" +#define RELAY_HTTP_503_SERVICE_UNAVAILABLE 503, "Service Unvavailable" + +#define RELAY_HTTP_ERROR_MISSING_PASSWORD "Missing password" +#define RELAY_HTTP_ERROR_INVALID_PASSWORD "Invalid password" +#define RELAY_HTTP_ERROR_MISSING_TOTP "Missing TOTP" +#define RELAY_HTTP_ERROR_INVALID_TOTP "Invalid TOTP" +#define RELAY_HTTP_ERROR_OUT_OF_MEMORY "Out of memory" + +struct t_relay_http_request +{ + enum t_relay_client_http_status status; /* HTTP status */ + char **raw; /* raw request */ + char *method; /* method (GET, POST, etc.) */ + char *path; /* path after method */ + char **path_items; /* list of items in path, eg: */ + /* "/api/a/b" => ["api", "a", "b"] */ + int num_path_items; /* number of path items */ + struct t_hashtable *params; /* optional parameters ("?p=a&q=b") */ + char *http_version; /* HTTP version (eg: "HTTP/1.1") */ + struct t_hashtable *headers; /* HTTP headers for websocket */ + /* and API protocol */ + struct t_hashtable *accept_encoding; /* allowed encoding for response */ + int content_length; /* value of header "Content-Length" */ + int body_size; /* size of HTTP body read so far */ + char *body; /* HTTP body (can be NULL) */ +}; + +extern void relay_http_request_reinit (struct t_relay_http_request *request); +extern struct t_relay_http_request *relay_http_request_alloc (); +extern int relay_http_parse_method_path (struct t_relay_http_request *request, + const char *method_path); +extern int relay_http_check_auth (struct t_relay_http_request *request); +extern void relay_http_recv (struct t_relay_client *client, const char *data); +extern int relay_http_send (struct t_relay_client *client, + int return_code, const char *message, + const char *headers, + const char *body, int body_size); +extern int relay_http_send_json (struct t_relay_client *client, + int return_code, + const char *message, + const char *headers, + const char *json_string); +extern int relay_http_send_error_json (struct t_relay_client *client, + int return_code, + const char *message, + const char *headers, + const char *format, ...); +extern void relay_http_request_free (struct t_relay_http_request *request); +extern void relay_http_print_log (struct t_relay_http_request *request); + +#endif /* WEECHAT_PLUGIN_RELAY_HTTP_H */ diff --git a/src/plugins/relay/relay-server.c b/src/plugins/relay/relay-server.c index c64551d82..b47df8b6f 100644 --- a/src/plugins/relay/relay-server.c +++ b/src/plugins/relay/relay-server.c @@ -52,7 +52,7 @@ struct t_relay_server *last_relay_server = NULL; * Examples: * * string ipv4 ipv6 tls unix protocol protocol_args - * --------------------------------------------------------------- + * ------------------------------------------------------------------- * irc.libera 1 1 0 0 irc libera * tls.irc.libera 1 1 1 0 irc libera * ipv4.irc.libera 1 0 0 0 irc libera @@ -63,6 +63,9 @@ struct t_relay_server *last_relay_server = NULL; * tls.weechat 1 1 1 0 weechat * ipv6.tls.weechat 0 1 1 0 weechat * unix.weechat 0 0 0 1 weechat + * tls.api 1 1 1 0 api + * ipv6.tls.api 0 1 1 0 api + * unix.api 0 0 0 1 api * * Note: *protocol and *protocol_args must be freed after use. */ @@ -801,6 +804,18 @@ relay_server_new (const char *protocol_string, enum t_relay_protocol protocol, if (!protocol_string) return NULL; +#ifndef HAVE_CJSON + if (protocol == RELAY_PROTOCOL_API) + { + weechat_printf (NULL, + _("%s%s: error: unable to add relay \"%s\" " + "(cJSON support is not enabled)"), + weechat_prefix ("error"), RELAY_PLUGIN_NAME, + protocol_string); + return NULL; + } +#endif /* HAVE_CJSON */ + /* look for duplicate ports/paths */ dup_server = (unix_socket) ? relay_server_search_path (path) : relay_server_search_port (port); diff --git a/src/plugins/relay/relay-websocket.c b/src/plugins/relay/relay-websocket.c index 326371ee3..4379119d8 100644 --- a/src/plugins/relay/relay-websocket.c +++ b/src/plugins/relay/relay-websocket.c @@ -28,6 +28,7 @@ #include "relay.h" #include "relay-client.h" #include "relay-config.h" +#include "relay-http.h" #include "relay-websocket.h" @@ -39,81 +40,44 @@ /* - * Checks if a message is a HTTP GET with resource "/weechat". + * Checks if a message is a HTTP GET with resource "/weechat" (for weechat + * protocol) or "/api" (for api protocol). * * Returns: - * 1: message is a HTTP GET with resource "/weechat" - * 0: message is NOT a HTTP GET with resource "/weechat" + * 1: message is a HTTP GET with appropriate resource + * 0: message is NOT a HTTP GET with appropriate resource */ int -relay_websocket_is_http_get_weechat (const char *message) +relay_websocket_is_valid_http_get (struct t_relay_client *client, + const char *message) { - /* the message must start with "GET /weechat" */ - if (strncmp (message, "GET /weechat", 12) != 0) + char string[128]; + int length; + + /* the message must start with "GET /weechat" or "GET /api" */ + snprintf (string, sizeof (string), + "GET /%s", relay_protocol_string[client->protocol]); + length = strlen (string); + + if (strncmp (message, string, length) != 0) return 0; - /* after "GET /weechat", only a new line or " HTTP" is allowed */ - if ((message[12] != '\r') && (message[12] != '\n') - && (strncmp (message + 12, " HTTP", 5) != 0)) + /* after "GET /weechat" or "GET /api", only a new line or " HTTP" is allowed */ + if ((message[length] != '\r') && (message[length] != '\n') + && (strncmp (message + length, " HTTP", 5) != 0)) { return 0; } - /* valid HTTP GET for resource "/weechat" */ + /* valid HTTP GET */ return 1; } -/* - * Saves a HTTP header in hashtable "http_header" of client. - */ - -void -relay_websocket_save_header (struct t_relay_client *client, - const char *message) -{ - char *pos, *name, *name_lower; - const char *ptr_value; - - /* ignore the "GET" request */ - if (strncmp (message, "GET ", 4) == 0) - return; - - pos = strchr (message, ':'); - - /* not a valid header */ - if (!pos || (pos == message)) - return; - - /* get header name, which is case-insensitive */ - name = weechat_strndup (message, pos - message); - if (!name) - return; - name_lower = weechat_string_tolower (name); - if (!name_lower) - { - free (name); - return; - } - - /* get pointer on header value */ - ptr_value = pos + 1; - while (ptr_value[0] == ' ') - { - ptr_value++; - } - - /* add header in the hashtable */ - weechat_hashtable_set (client->http_headers, name_lower, ptr_value); - - free (name); - free (name_lower); -} - /* * Checks if a client handshake is valid. * - * A websocket query looks like: + * A websocket query looks like (weechat protocol): * GET /weechat HTTP/1.1 * Upgrade: websocket * Connection: Upgrade @@ -126,6 +90,20 @@ relay_websocket_save_header (struct t_relay_client *client, * Sec-WebSocket-Extensions: x-webkit-deflate-frame * Cookie: csrftoken=acb65377798f32dc377ebb50316a12b5 * + * A websocket query looks like (api protocol): + * GET /api HTTP/1.1 + * Upgrade: websocket + * Connection: Upgrade + * Host: myhost:5000 + * Origin: https://example.org + * Pragma: no-cache + * Cache-Control: no-cache + * Sec-WebSocket-Key: fo1J9uHSsrfDP3BkwUylzQ== + * Sec-WebSocket-Version: 13 + * Sec-WebSocket-Extensions: x-webkit-deflate-frame + * Cookie: csrftoken=acb65377798f32dc377ebb50316a12b5 + * + * * Expected HTTP headers with values are: * * header | value @@ -149,20 +127,20 @@ relay_websocket_client_handshake_valid (struct t_relay_client *client) const char *value; /* check if we have header "Upgrade" with value "websocket" */ - value = weechat_hashtable_get (client->http_headers, "upgrade"); + value = weechat_hashtable_get (client->http_req->headers, "upgrade"); if (!value) return -1; if (weechat_strcasecmp (value, "websocket") != 0) return -1; /* check if we have header "Sec-WebSocket-Key" with non-empty value */ - value = weechat_hashtable_get (client->http_headers, "sec-websocket-key"); + value = weechat_hashtable_get (client->http_req->headers, "sec-websocket-key"); if (!value || !value[0]) return -1; if (relay_config_regex_websocket_allowed_origins) { - value = weechat_hashtable_get (client->http_headers, "origin"); + value = weechat_hashtable_get (client->http_req->headers, "origin"); if (!value || !value[0]) return -2; if (regexec (relay_config_regex_websocket_allowed_origins, value, 0, @@ -196,7 +174,7 @@ relay_websocket_build_handshake (struct t_relay_client *client) char *key, sec_websocket_accept[128], handshake[1024], hash[160 / 8]; int length, hash_size; - sec_websocket_key = weechat_hashtable_get (client->http_headers, + sec_websocket_key = weechat_hashtable_get (client->http_req->headers, "sec-websocket-key"); if (!sec_websocket_key || !sec_websocket_key[0]) return NULL; @@ -238,31 +216,6 @@ relay_websocket_build_handshake (struct t_relay_client *client) return strdup (handshake); } -/* - * Sends a HTTP message to client. - * - * Argument "http" is a HTTP code + message, for example: - * "403 Forbidden". - */ - -void -relay_websocket_send_http (struct t_relay_client *client, - const char *http) -{ - char *message; - int length; - - length = 32 + strlen (http) + 1; - message = malloc (length); - if (message) - { - snprintf (message, length, "HTTP/1.1 %s\r\n\r\n", http); - relay_client_send (client, RELAY_CLIENT_MSG_STANDARD, - message, strlen (message), NULL); - free (message); - } -} - /* * Decodes a websocket frame. * diff --git a/src/plugins/relay/relay-websocket.h b/src/plugins/relay/relay-websocket.h index c80d731c5..18e2002fb 100644 --- a/src/plugins/relay/relay-websocket.h +++ b/src/plugins/relay/relay-websocket.h @@ -27,13 +27,10 @@ #define WEBSOCKET_FRAME_OPCODE_PING 0x09 #define WEBSOCKET_FRAME_OPCODE_PONG 0x0A -extern int relay_websocket_is_http_get_weechat (const char *message); -extern void relay_websocket_save_header (struct t_relay_client *client, - const char *message); +extern int relay_websocket_is_valid_http_get (struct t_relay_client *client, + const char *message); extern int relay_websocket_client_handshake_valid (struct t_relay_client *client); extern char *relay_websocket_build_handshake (struct t_relay_client *client); -extern void relay_websocket_send_http (struct t_relay_client *client, - const char *http); extern int relay_websocket_decode_frame (const unsigned char *buffer, unsigned long long length, unsigned char *decoded, diff --git a/src/plugins/relay/relay.c b/src/plugins/relay/relay.c index 9c7e8fff0..ca22dad69 100644 --- a/src/plugins/relay/relay.c +++ b/src/plugins/relay/relay.c @@ -47,7 +47,7 @@ WEECHAT_PLUGIN_PRIORITY(RELAY_PLUGIN_PRIORITY); struct t_weechat_plugin *weechat_relay_plugin = NULL; char *relay_protocol_string[] = /* strings for protocols */ -{ "weechat", "irc" }; +{ "weechat", "irc", "api" }; struct t_hdata *relay_hdata_buffer = NULL; struct t_hdata *relay_hdata_lines = NULL; diff --git a/src/plugins/relay/relay.h b/src/plugins/relay/relay.h index a848ba56c..70c743c6e 100644 --- a/src/plugins/relay/relay.h +++ b/src/plugins/relay/relay.h @@ -43,6 +43,7 @@ enum t_relay_protocol { RELAY_PROTOCOL_WEECHAT = 0, /* WeeChat protocol */ RELAY_PROTOCOL_IRC, /* IRC protocol (IRC proxy) */ + RELAY_PROTOCOL_API, /* HTTP REST API */ /* number of relay protocols */ RELAY_NUM_PROTOCOLS, }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7bd01caa3..13574ba26 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -107,6 +107,7 @@ endif() if (ENABLE_RELAY) list(APPEND LIB_WEECHAT_UNIT_TESTS_PLUGINS_SRC unit/plugins/relay/test-relay-auth.cpp + unit/plugins/relay/test-relay-http.cpp unit/plugins/relay/irc/test-relay-irc.cpp ) endif() diff --git a/tests/unit/plugins/relay/test-relay-http.cpp b/tests/unit/plugins/relay/test-relay-http.cpp new file mode 100644 index 000000000..b3ad96890 --- /dev/null +++ b/tests/unit/plugins/relay/test-relay-http.cpp @@ -0,0 +1,742 @@ +/* + * test-relay-http.cpp - test HTTP functions + * + * Copyright (C) 2023-2024 Sébastien Helleu + * + * This file is part of WeeChat, the extensible chat client. + * + * WeeChat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * WeeChat is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with WeeChat. If not, see . + */ + +#include "CppUTest/TestHarness.h" + +#include "tests/tests.h" + +extern "C" +{ +#include +#include +#include +#include "src/core/wee-config-file.h" +#include "src/core/wee-hashtable.h" +#include "src/core/wee-hook.h" +#include "src/core/wee-string.h" +#include "src/plugins/relay/relay-config.h" +#include "src/plugins/relay/relay-http.h" +#include "src/plugins/weechat-plugin.h" + +extern char *relay_http_url_decode (const char *url); +extern void relay_http_parse_path (const char *url, + char ***paths, int *num_paths, + struct t_hashtable *params); +extern int relay_http_parse_header (struct t_relay_http_request *request, + const char *header); +extern void relay_http_add_to_body (struct t_relay_http_request *request, + char **partial_message); +extern char *relay_http_compress (struct t_relay_http_request *request, + const char *data, int data_size, + int *compressed_size, + char *http_content_encoding, + int http_content_encoding_size); +} + +#define WEE_PARSE_PATH(__path) \ + paths = (char **)0x1; \ + num_paths = -1; \ + hashtable_remove_all (params); \ + relay_http_parse_path (__path, &paths, &num_paths, params); + +TEST_GROUP(RelayHttp) +{ +}; + +/* + * Tests functions: + * relay_http_request_alloc + * relay_http_request_reinit + * relay_http_request_free + */ + +TEST(RelayHttp, AllocReinitFree) +{ + struct t_relay_http_request *request; + + request = relay_http_request_alloc (); + CHECK(request); + + LONGS_EQUAL(RELAY_HTTP_METHOD, request->status); + CHECK(request->raw); + STRCMP_EQUAL("", *(request->raw)); + POINTERS_EQUAL(NULL, request->method); + POINTERS_EQUAL(NULL, request->path); + POINTERS_EQUAL(NULL, request->path_items); + LONGS_EQUAL(0, request->num_path_items); + CHECK(request->params); + LONGS_EQUAL(0, request->params->items_count); + POINTERS_EQUAL(NULL, request->http_version); + CHECK(request->headers); + LONGS_EQUAL(0, request->headers->items_count); + CHECK(request->accept_encoding); + LONGS_EQUAL(0, request->accept_encoding->items_count); + LONGS_EQUAL(0, request->content_length); + LONGS_EQUAL(0, request->body_size); + POINTERS_EQUAL(NULL, request->body); + + request->status = RELAY_HTTP_HEADERS; + string_dyn_concat (request->raw, "test", -1); + request->method = strdup ("test"); + request->path = strdup ("test"); + request->path_items = string_split ("test,1,2,3", ",", NULL, 0, 0, &request->num_path_items); + hashtable_set (request->params, "test", "value"); + request->http_version = strdup ("HTTP/1.1"); + hashtable_set (request->headers, "x-test", "value"); + hashtable_set (request->accept_encoding, "gzip", ""); + request->content_length = 100; + request->body_size = 16; + request->body = (char *)malloc (16); + memset (request->body, 0, 16); + + relay_http_request_reinit (request); + + LONGS_EQUAL(RELAY_HTTP_METHOD, request->status); + CHECK(request->raw); + STRCMP_EQUAL("", *(request->raw)); + POINTERS_EQUAL(NULL, request->method); + POINTERS_EQUAL(NULL, request->path); + POINTERS_EQUAL(NULL, request->path_items); + LONGS_EQUAL(0, request->num_path_items); + CHECK(request->params); + LONGS_EQUAL(0, request->params->items_count); + POINTERS_EQUAL(NULL, request->http_version); + CHECK(request->headers); + LONGS_EQUAL(0, request->headers->items_count); + CHECK(request->accept_encoding); + LONGS_EQUAL(0, request->accept_encoding->items_count); + LONGS_EQUAL(0, request->content_length); + LONGS_EQUAL(0, request->body_size); + POINTERS_EQUAL(NULL, request->body); + + relay_http_request_free (request); +} + +/* + * Tests functions: + * relay_http_url_decode + */ + +TEST(RelayHttp, UrlDecode) +{ + char *str; + + POINTERS_EQUAL(NULL, relay_http_url_decode (NULL)); + WEE_TEST_STR("", relay_http_url_decode ("")); + WEE_TEST_STR("test", relay_http_url_decode ("test")); + WEE_TEST_STR("%", relay_http_url_decode ("%")); + WEE_TEST_STR("%%", relay_http_url_decode ("%%")); + WEE_TEST_STR("%test", relay_http_url_decode ("%test")); + WEE_TEST_STR("#test", relay_http_url_decode ("#test")); + WEE_TEST_STR("#test", relay_http_url_decode ("%23test")); + WEE_TEST_STR("%#test", relay_http_url_decode ("%%23test")); + WEE_TEST_STR("%#test hello", relay_http_url_decode ("%%23test%20hello")); + WEE_TEST_STR("test+z", relay_http_url_decode ("test%2bz")); + WEE_TEST_STR("test+z", relay_http_url_decode ("test%2Bz")); + WEE_TEST_STR("test*", relay_http_url_decode ("test%2a")); +} + +/* + * Tests functions: + * relay_http_parse_path + */ + +TEST(RelayHttp, ParsePath) +{ + char **paths; + int num_paths; + struct t_hashtable *params; + + params = hashtable_new ( + 32, + WEECHAT_HASHTABLE_STRING, + WEECHAT_HASHTABLE_STRING, + NULL, NULL); + + WEE_PARSE_PATH(NULL); + POINTERS_EQUAL(NULL, paths); + LONGS_EQUAL(0, num_paths); + LONGS_EQUAL(0, params->items_count); + + WEE_PARSE_PATH(""); + POINTERS_EQUAL(NULL, paths); + LONGS_EQUAL(0, num_paths); + LONGS_EQUAL(0, params->items_count); + + WEE_PARSE_PATH("api"); + CHECK(paths); + LONGS_EQUAL(1, num_paths); + STRCMP_EQUAL("api", paths[0]); + POINTERS_EQUAL(NULL, paths[1]); + LONGS_EQUAL(0, params->items_count); + + WEE_PARSE_PATH("/api"); + CHECK(paths); + LONGS_EQUAL(1, num_paths); + STRCMP_EQUAL("api", paths[0]); + POINTERS_EQUAL(NULL, paths[1]); + LONGS_EQUAL(0, params->items_count); + + WEE_PARSE_PATH("/api/"); + CHECK(paths); + LONGS_EQUAL(1, num_paths); + STRCMP_EQUAL("api", paths[0]); + POINTERS_EQUAL(NULL, paths[1]); + LONGS_EQUAL(0, params->items_count); + + WEE_PARSE_PATH("/api/buffers"); + CHECK(paths); + LONGS_EQUAL(2, num_paths); + STRCMP_EQUAL("api", paths[0]); + STRCMP_EQUAL("buffers", paths[1]); + POINTERS_EQUAL(NULL, paths[2]); + LONGS_EQUAL(0, params->items_count); + + WEE_PARSE_PATH("/api/buffers?"); + CHECK(paths); + LONGS_EQUAL(2, num_paths); + STRCMP_EQUAL("api", paths[0]); + STRCMP_EQUAL("buffers", paths[1]); + POINTERS_EQUAL(NULL, paths[2]); + LONGS_EQUAL(0, params->items_count); + + WEE_PARSE_PATH("/api/buffers?param"); + CHECK(paths); + LONGS_EQUAL(2, num_paths); + STRCMP_EQUAL("api", paths[0]); + STRCMP_EQUAL("buffers", paths[1]); + POINTERS_EQUAL(NULL, paths[2]); + LONGS_EQUAL(1, params->items_count); + STRCMP_EQUAL("", (const char *)hashtable_get (params, "param")); + + WEE_PARSE_PATH("/api/buffers?param="); + CHECK(paths); + LONGS_EQUAL(2, num_paths); + STRCMP_EQUAL("api", paths[0]); + STRCMP_EQUAL("buffers", paths[1]); + POINTERS_EQUAL(NULL, paths[2]); + LONGS_EQUAL(1, params->items_count); + STRCMP_EQUAL("", (const char *)hashtable_get (params, "param")); + + WEE_PARSE_PATH("/api/buffers?param=off"); + CHECK(paths); + LONGS_EQUAL(2, num_paths); + STRCMP_EQUAL("api", paths[0]); + STRCMP_EQUAL("buffers", paths[1]); + POINTERS_EQUAL(NULL, paths[2]); + LONGS_EQUAL(1, params->items_count); + STRCMP_EQUAL("off", (const char *)hashtable_get (params, "param")); + + WEE_PARSE_PATH("/api/buffers?param=off&test=value2"); + CHECK(paths); + LONGS_EQUAL(2, num_paths); + STRCMP_EQUAL("api", paths[0]); + STRCMP_EQUAL("buffers", paths[1]); + POINTERS_EQUAL(NULL, paths[2]); + LONGS_EQUAL(2, params->items_count); + STRCMP_EQUAL("off", (const char *)hashtable_get (params, "param")); + STRCMP_EQUAL("value2", (const char *)hashtable_get (params, "test")); + + WEE_PARSE_PATH("/api/buffers/irc.libera.%23weechat?param=off&test=value%202"); + CHECK(paths); + LONGS_EQUAL(3, num_paths); + STRCMP_EQUAL("api", paths[0]); + STRCMP_EQUAL("buffers", paths[1]); + STRCMP_EQUAL("irc.libera.#weechat", paths[2]); + POINTERS_EQUAL(NULL, paths[3]); + LONGS_EQUAL(2, params->items_count); + STRCMP_EQUAL("off", (const char *)hashtable_get (params, "param")); + STRCMP_EQUAL("value 2", (const char *)hashtable_get (params, "test")); + + hashtable_free (params); +} + +/* + * Tests functions: + * relay_http_parse_method_path + */ + +TEST(RelayHttp, ParseMethodPath) +{ + struct t_relay_http_request *request; + + request = relay_http_request_alloc (); + CHECK(request); + relay_http_parse_method_path (request, NULL); + LONGS_EQUAL(RELAY_HTTP_METHOD, request->status); + STRCMP_EQUAL("", *(request->raw)); + POINTERS_EQUAL(NULL, request->path); + POINTERS_EQUAL(NULL, request->path_items); + LONGS_EQUAL(0, request->num_path_items); + LONGS_EQUAL(0, request->params->items_count); + POINTERS_EQUAL(NULL, request->http_version); + LONGS_EQUAL(0, request->headers->items_count); + LONGS_EQUAL(0, request->accept_encoding->items_count); + LONGS_EQUAL(0, request->content_length); + LONGS_EQUAL(0, request->body_size); + POINTERS_EQUAL(NULL, request->body); + free (request); + + request = relay_http_request_alloc (); + CHECK(request); + relay_http_parse_method_path (request, ""); + LONGS_EQUAL(RELAY_HTTP_METHOD, request->status); + STRCMP_EQUAL("", *(request->raw)); + POINTERS_EQUAL(NULL, request->path); + POINTERS_EQUAL(NULL, request->path_items); + LONGS_EQUAL(0, request->num_path_items); + LONGS_EQUAL(0, request->params->items_count); + POINTERS_EQUAL(NULL, request->http_version); + LONGS_EQUAL(0, request->headers->items_count); + LONGS_EQUAL(0, request->accept_encoding->items_count); + LONGS_EQUAL(0, request->content_length); + LONGS_EQUAL(0, request->body_size); + POINTERS_EQUAL(NULL, request->body); + free (request); + + request = relay_http_request_alloc (); + CHECK(request); + relay_http_parse_method_path (request, "GET"); + LONGS_EQUAL(RELAY_HTTP_END, request->status); + STRCMP_EQUAL("GET\n", *(request->raw)); + POINTERS_EQUAL(NULL, request->path); + POINTERS_EQUAL(NULL, request->path_items); + LONGS_EQUAL(0, request->num_path_items); + LONGS_EQUAL(0, request->params->items_count); + POINTERS_EQUAL(NULL, request->http_version); + LONGS_EQUAL(0, request->headers->items_count); + LONGS_EQUAL(0, request->accept_encoding->items_count); + LONGS_EQUAL(0, request->content_length); + LONGS_EQUAL(0, request->body_size); + POINTERS_EQUAL(NULL, request->body); + free (request); + + request = relay_http_request_alloc (); + CHECK(request); + relay_http_parse_method_path (request, "GET /api"); + LONGS_EQUAL(RELAY_HTTP_HEADERS, request->status); + STRCMP_EQUAL("GET /api\n", *(request->raw)); + STRCMP_EQUAL("/api", request->path); + CHECK(request->path_items); + STRCMP_EQUAL("api", request->path_items[0]); + POINTERS_EQUAL(NULL, request->path_items[1]); + LONGS_EQUAL(1, request->num_path_items); + LONGS_EQUAL(0, request->params->items_count); + POINTERS_EQUAL(NULL, request->http_version); + LONGS_EQUAL(0, request->headers->items_count); + LONGS_EQUAL(0, request->accept_encoding->items_count); + LONGS_EQUAL(0, request->content_length); + LONGS_EQUAL(0, request->body_size); + POINTERS_EQUAL(NULL, request->body); + free (request); + + request = relay_http_request_alloc (); + CHECK(request); + relay_http_parse_method_path (request, "GET /api/buffers HTTP/1.1"); + LONGS_EQUAL(RELAY_HTTP_HEADERS, request->status); + STRCMP_EQUAL("GET /api/buffers HTTP/1.1\n", *(request->raw)); + STRCMP_EQUAL("/api/buffers", request->path); + CHECK(request->path_items); + STRCMP_EQUAL("api", request->path_items[0]); + STRCMP_EQUAL("buffers", request->path_items[1]); + POINTERS_EQUAL(NULL, request->path_items[2]); + LONGS_EQUAL(2, request->num_path_items); + LONGS_EQUAL(0, request->params->items_count); + STRCMP_EQUAL("HTTP/1.1", request->http_version); + LONGS_EQUAL(0, request->headers->items_count); + LONGS_EQUAL(0, request->accept_encoding->items_count); + LONGS_EQUAL(0, request->content_length); + LONGS_EQUAL(0, request->body_size); + POINTERS_EQUAL(NULL, request->body); + free (request); + + request = relay_http_request_alloc (); + CHECK(request); + relay_http_parse_method_path (request, "GET /api/buffers?test=1&var=abc HTTP/1.1"); + /* do it 2 times, to be sure it has no side effect */ + relay_http_parse_method_path (request, "GET /api/buffers?test=1&var=abc HTTP/1.1"); + LONGS_EQUAL(RELAY_HTTP_HEADERS, request->status); + STRCMP_EQUAL("GET /api/buffers?test=1&var=abc HTTP/1.1\n" + "GET /api/buffers?test=1&var=abc HTTP/1.1\n", + *(request->raw)); + STRCMP_EQUAL("/api/buffers?test=1&var=abc", request->path); + CHECK(request->path_items); + STRCMP_EQUAL("api", request->path_items[0]); + STRCMP_EQUAL("buffers", request->path_items[1]); + POINTERS_EQUAL(NULL, request->path_items[2]); + LONGS_EQUAL(2, request->num_path_items); + LONGS_EQUAL(2, request->params->items_count); + STRCMP_EQUAL("1", (const char *)hashtable_get (request->params, "test")); + STRCMP_EQUAL("abc", (const char *)hashtable_get (request->params, "var")); + STRCMP_EQUAL("HTTP/1.1", request->http_version); + LONGS_EQUAL(0, request->headers->items_count); + LONGS_EQUAL(0, request->accept_encoding->items_count); + LONGS_EQUAL(0, request->content_length); + LONGS_EQUAL(0, request->body_size); + POINTERS_EQUAL(NULL, request->body); + free (request); +} + +/* + * Tests functions: + * relay_http_parse_header + */ + +TEST(RelayHttp, ParseHeader) +{ + struct t_relay_http_request *request; + + request = relay_http_request_alloc (); + CHECK(request); + relay_http_parse_method_path (request, "GET /api/version"); + relay_http_parse_header (request, NULL); + LONGS_EQUAL(RELAY_HTTP_END, request->status); + STRCMP_EQUAL("GET /api/version\n" + "\n", + *(request->raw)); + LONGS_EQUAL(0, request->headers->items_count); + free (request); + + request = relay_http_request_alloc (); + CHECK(request); + relay_http_parse_method_path (request, "GET /api/version"); + relay_http_parse_header (request, ""); + LONGS_EQUAL(RELAY_HTTP_END, request->status); + STRCMP_EQUAL("GET /api/version\n" + "\n", + *(request->raw)); + LONGS_EQUAL(0, request->headers->items_count); + free (request); + + request = relay_http_request_alloc (); + CHECK(request); + relay_http_parse_method_path (request, "GET /api/version"); + relay_http_parse_header (request, "Test"); + LONGS_EQUAL(RELAY_HTTP_HEADERS, request->status); + STRCMP_EQUAL("GET /api/version\n" + "Test\n", + *(request->raw)); + LONGS_EQUAL(0, request->headers->items_count); + free (request); + + request = relay_http_request_alloc (); + CHECK(request); + relay_http_parse_method_path (request, "GET /api/version"); + relay_http_parse_header (request, "X-Test: value"); + LONGS_EQUAL(RELAY_HTTP_HEADERS, request->status); + STRCMP_EQUAL("GET /api/version\n" + "X-Test: value\n", + *(request->raw)); + LONGS_EQUAL(1, request->headers->items_count); + STRCMP_EQUAL("value", (const char *)hashtable_get (request->headers, "x-test")); + free (request); + + request = relay_http_request_alloc (); + CHECK(request); + relay_http_parse_method_path (request, "GET /api/version"); + relay_http_parse_header (request, "Accept-Encoding: gzip, zstd, br"); + LONGS_EQUAL(RELAY_HTTP_HEADERS, request->status); + STRCMP_EQUAL("GET /api/version\n" + "Accept-Encoding: gzip, zstd, br\n", + *(request->raw)); + LONGS_EQUAL(1, request->headers->items_count); + STRCMP_EQUAL("gzip, zstd, br", + (const char *)hashtable_get (request->headers, "accept-encoding")); + LONGS_EQUAL(3, request->accept_encoding->items_count); + CHECK(hashtable_has_key (request->accept_encoding, "gzip")); + CHECK(hashtable_has_key (request->accept_encoding, "zstd")); + CHECK(hashtable_has_key (request->accept_encoding, "br")); + free (request); + + request = relay_http_request_alloc (); + CHECK(request); + relay_http_parse_method_path (request, "GET /api/version"); + relay_http_parse_header (request, "Content-Length: 123"); + LONGS_EQUAL(RELAY_HTTP_HEADERS, request->status); + STRCMP_EQUAL("GET /api/version\n" + "Content-Length: 123\n", + *(request->raw)); + LONGS_EQUAL(1, request->headers->items_count); + LONGS_EQUAL(123, request->content_length); + free (request); +} + +/* + * Tests functions: + * relay_http_add_to_body + */ + +TEST(RelayHttp, AddToBody) +{ + struct t_relay_http_request *request; + const char *body_part1 = "abc", *body_part2 = "defghij"; + char *partial_message; + + request = relay_http_request_alloc (); + CHECK(request); + LONGS_EQUAL(RELAY_HTTP_METHOD, request->status); + relay_http_parse_method_path (request, "GET /api/version"); + LONGS_EQUAL(RELAY_HTTP_HEADERS, request->status); + relay_http_parse_header (request, "Content-Length: 10"); + relay_http_parse_header (request, ""); + LONGS_EQUAL(RELAY_HTTP_BODY, request->status); + LONGS_EQUAL(10, request->content_length); + LONGS_EQUAL(0, request->body_size); + + partial_message = strdup (body_part1); + relay_http_add_to_body (request, &partial_message); + LONGS_EQUAL(RELAY_HTTP_BODY, request->status); + LONGS_EQUAL(3, request->body_size); + MEMCMP_EQUAL("abc", request->body, 3); + POINTERS_EQUAL(NULL, partial_message); + + partial_message = strdup (body_part2); + relay_http_add_to_body (request, &partial_message); + LONGS_EQUAL(RELAY_HTTP_END, request->status); + LONGS_EQUAL(10, request->body_size); + MEMCMP_EQUAL("abcdefghij", request->body, 10); + POINTERS_EQUAL(NULL, partial_message); + + free (request); + + request = relay_http_request_alloc (); + CHECK(request); + LONGS_EQUAL(RELAY_HTTP_METHOD, request->status); + relay_http_parse_method_path (request, "GET /api/version"); + LONGS_EQUAL(RELAY_HTTP_HEADERS, request->status); + relay_http_parse_header (request, "Content-Length: 5"); + relay_http_parse_header (request, ""); + LONGS_EQUAL(RELAY_HTTP_BODY, request->status); + LONGS_EQUAL(5, request->content_length); + LONGS_EQUAL(0, request->body_size); + + partial_message = strdup (body_part1); + relay_http_add_to_body (request, &partial_message); + LONGS_EQUAL(RELAY_HTTP_BODY, request->status); + LONGS_EQUAL(3, request->body_size); + MEMCMP_EQUAL("abc", request->body, 3); + POINTERS_EQUAL(NULL, partial_message); + + partial_message = strdup (body_part2); + relay_http_add_to_body (request, &partial_message); + LONGS_EQUAL(RELAY_HTTP_END, request->status); + LONGS_EQUAL(5, request->body_size); + MEMCMP_EQUAL("abcde", request->body, 5); + STRCMP_EQUAL("fghij", partial_message); + free (partial_message); + + free (request); +} + +/* + * Tests functions: + * relay_http_check_auth + */ + +TEST(RelayHttp, CheckAuth) +{ + struct t_relay_http_request *request; + char *totp, *totp2; + + config_file_option_set (relay_config_network_password, "secret_password", 1); + + request = relay_http_request_alloc (); + CHECK(request); + + /* test password */ + LONGS_EQUAL(-1, relay_http_check_auth (request)); + hashtable_set (request->headers, "authorization", "Basic "); + LONGS_EQUAL(-2, relay_http_check_auth (request)); + hashtable_set (request->headers, "authorization", "Basic \u26c4"); + LONGS_EQUAL(-2, relay_http_check_auth (request)); + /* set invalid user/password: "weechat:test" */ + hashtable_set (request->headers, "authorization", "Basic d2VlY2hhdDp0ZXN0"); + LONGS_EQUAL(-2, relay_http_check_auth (request)); + /* set valid user/password: "weechat:secret_password" */ + hashtable_set (request->headers, + "authorization", "Basic d2VlY2hhdDpzZWNyZXRfcGFzc3dvcmQ"); + LONGS_EQUAL(0, relay_http_check_auth (request)); + + /* test missing/invalid TOTP */ + config_file_option_set (relay_config_network_totp_secret, "secretbase32", 1); + config_file_option_set (relay_config_network_totp_window, "1", 1); + LONGS_EQUAL(-3, relay_http_check_auth (request)); + totp = hook_info_get (NULL, "totp_generate", "secretbase32"); + CHECK(totp); + totp2 = strdup (totp); + totp2[0] = (totp2[0] == '1') ? '2' : '1'; + hashtable_set (request->headers, "x-weechat-totp", totp2); + LONGS_EQUAL(-4, relay_http_check_auth (request)); + hashtable_set (request->headers, "x-weechat-totp", totp); + LONGS_EQUAL(0, relay_http_check_auth (request)); + free (totp); + free (totp2); + config_file_option_reset (relay_config_network_totp_secret, 1); + config_file_option_reset (relay_config_network_totp_window, 1); + + config_file_option_reset (relay_config_network_password, 1); +} + +/* + * Tests functions: + * relay_http_process_websocket + */ + +TEST(RelayHttp, ProcessWebsocket) +{ + /* TODO: write tests */ +} + +/* + * Tests functions: + * relay_http_process_request + */ + +TEST(RelayHttp, ProcessRequest) +{ + /* TODO: write tests */ +} + +/* + * Tests functions: + * relay_http_recv + */ + +TEST(RelayHttp, Recv) +{ + /* TODO: write tests */ +} + +/* + * Tests functions: + * relay_http_compress + */ + +TEST(RelayHttp, Compress) +{ + struct t_relay_http_request *request; + char body[256], *buffer, encoding[256]; + int i, size; + + request = relay_http_request_alloc (); + + for (i = 0; i < (int)sizeof (body); i++) + { + body[i] = i % 64; + } + + hashtable_remove_all (request->accept_encoding); + buffer = (char *)0x1; + size = -1; + buffer = relay_http_compress (request, NULL, 0, &size, NULL, 0); + POINTERS_EQUAL(NULL, buffer); + LONGS_EQUAL(0, size); + + hashtable_remove_all (request->accept_encoding); + buffer = (char *)0x1; + size = -1; + snprintf (encoding, sizeof (encoding), "test"); + buffer = relay_http_compress (request, NULL, 0, &size, + encoding, sizeof (encoding)); + POINTERS_EQUAL(NULL, buffer); + LONGS_EQUAL(0, size); + STRCMP_EQUAL("", encoding); + + /* no "Accept-Encoding" header was received => no compression */ + hashtable_remove_all (request->accept_encoding); + buffer = (char *)0x1; + size = -1; + snprintf (encoding, sizeof (encoding), "test"); + buffer = relay_http_compress (request, body, sizeof (body), &size, + encoding, sizeof (encoding)); + POINTERS_EQUAL(NULL, buffer); + LONGS_EQUAL(0, size); + STRCMP_EQUAL("", encoding); + + /* "Accept-Encoding: gzip" => gzip compression */ + hashtable_remove_all (request->accept_encoding); + hashtable_set (request->accept_encoding, "gzip", ""); + buffer = (char *)0x1; + size = -1; + snprintf (encoding, sizeof (encoding), "test"); + buffer = relay_http_compress (request, body, sizeof (body), &size, + encoding, sizeof (encoding)); + CHECK(buffer); + CHECK((size > 0) && (size < (int)sizeof (body))); + STRCMP_EQUAL("Content-Encoding: gzip\r\n", encoding); + free (buffer); + +#ifdef HAVE_ZSTD + /* "Accept-Encoding: gzip, zstd" => zstd compression */ + hashtable_remove_all (request->accept_encoding); + hashtable_set (request->accept_encoding, "gzip", ""); + hashtable_set (request->accept_encoding, "zstd", ""); + buffer = (char *)0x1; + size = -1; + snprintf (encoding, sizeof (encoding), "test"); + buffer = relay_http_compress (request, body, sizeof (body), &size, + encoding, sizeof (encoding)); + CHECK(buffer); + CHECK((size > 0) && (size < (int)sizeof (body))); + STRCMP_EQUAL("Content-Encoding: zstd\r\n", encoding); + free (buffer); +#endif +} + +/* + * Tests functions: + * relay_http_send + */ + +TEST(RelayHttp, Send) +{ + /* TODO: write tests */ +} + +/* + * Tests functions: + * relay_http_send_json + */ + +TEST(RelayHttp, SendJson) +{ + /* TODO: write tests */ +} + +/* + * Tests functions: + * relay_http_send_error_json + */ + +TEST(RelayHttp, SendErrorJson) +{ + /* TODO: write tests */ +} + +/* + * Tests functions: + * relay_http_print_log + */ + +TEST(RelayHttp, PrintLog) +{ + /* TODO: write tests */ +} diff --git a/tools/debian/patches/weechat_ubuntu_bionic.patch b/tools/debian/patches/weechat_ubuntu_bionic.patch index 53405db88..94c0aed2b 100644 --- a/tools/debian/patches/weechat_ubuntu_bionic.patch +++ b/tools/debian/patches/weechat_ubuntu_bionic.patch @@ -6,10 +6,10 @@ index 48082f72f..b4de39476 100644 -12 +11 diff --git a/debian-devel/control b/debian-devel/control -index 4229b00f6..41addd80b 100644 +index a5d24b6a8..81ae98045 100644 --- a/debian-devel/control +++ b/debian-devel/control -@@ -5,17 +5,17 @@ Maintainer: Sébastien Helleu +@@ -5,24 +5,23 @@ Maintainer: Sébastien Helleu Build-Depends: asciidoctor (>= 1.5.4), ruby-pygments.rb, @@ -31,6 +31,26 @@ index 4229b00f6..41addd80b 100644 libxml2-dev, libcurl4-gnutls-dev, libgcrypt20-dev, + libgnutls28-dev, + libzstd-dev, +- zlib1g-dev, +- libcjson-dev ++ zlib1g-dev + Standards-Version: 4.6.2 + Homepage: https://weechat.org/ + Vcs-Git: https://salsa.debian.org/kolter/weechat.git +diff --git a/debian-devel/rules b/debian-devel/rules +index cab713c93..d2756333b 100755 +--- a/debian-devel/rules ++++ b/debian-devel/rules +@@ -8,6 +8,7 @@ override_dh_auto_configure: + dh_auto_configure --buildsystem=cmake -- \ + -DCMAKE_INSTALL_PREFIX:FILEPATH=/usr \ + -DLIBDIR=/usr/lib/${DEB_HOST_MULTIARCH} \ ++ -DENABLE_CJSON:BOOL=OFF \ + -DENABLE_DOC:BOOL=ON \ + -DENABLE_MAN:BOOL=ON \ + -DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo \ diff --git a/debian-stable/compat b/debian-stable/compat index 48082f72f..b4de39476 100644 --- a/debian-stable/compat @@ -39,10 +59,10 @@ index 48082f72f..b4de39476 100644 -12 +11 diff --git a/debian-stable/control b/debian-stable/control -index ad3f771b7..e88f167b8 100644 +index a75e6fee5..b39bf88dc 100644 --- a/debian-stable/control +++ b/debian-stable/control -@@ -5,17 +5,17 @@ Maintainer: Emmanuel Bouthenot +@@ -5,24 +5,23 @@ Maintainer: Emmanuel Bouthenot Build-Depends: asciidoctor (>= 1.5.4), ruby-pygments.rb, @@ -64,3 +84,23 @@ index ad3f771b7..e88f167b8 100644 libxml2-dev, libcurl4-gnutls-dev, libgcrypt20-dev, + libgnutls28-dev, + libzstd-dev, +- zlib1g-dev, +- libcjson-dev ++ zlib1g-dev + Standards-Version: 4.6.2 + Homepage: https://weechat.org/ + Vcs-Git: https://salsa.debian.org/kolter/weechat.git +diff --git a/debian-stable/rules b/debian-stable/rules +index cab713c93..d2756333b 100755 +--- a/debian-stable/rules ++++ b/debian-stable/rules +@@ -8,6 +8,7 @@ override_dh_auto_configure: + dh_auto_configure --buildsystem=cmake -- \ + -DCMAKE_INSTALL_PREFIX:FILEPATH=/usr \ + -DLIBDIR=/usr/lib/${DEB_HOST_MULTIARCH} \ ++ -DENABLE_CJSON:BOOL=OFF \ + -DENABLE_DOC:BOOL=ON \ + -DENABLE_MAN:BOOL=ON \ + -DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo \ diff --git a/weechat.cygport.in b/weechat.cygport.in index 54a434310..004db3ef6 100644 --- a/weechat.cygport.in +++ b/weechat.cygport.in @@ -61,7 +61,10 @@ zlib-devel" # 1. set prefix to /usr # 2. enable build of man pages and documentation # -CYGCMAKE_ARGS="-DCMAKE_INSTALL_PREFIX=/usr -DENABLE_MAN=ON -DENABLE_DOC=ON" +CYGCMAKE_ARGS="-DCMAKE_INSTALL_PREFIX=/usr \ +-DENABLE_CJSON=OFF \ +-DENABLE_MAN=ON \ +-DENABLE_DOC=ON" PKG_IGNORE=" etc/postinstall/weechat.sh