Compare commits

..

6 Commits

Author SHA1 Message Date
2c8bd867b9 fix(xmpp): treat disco#info result without 'from' as from the server
All checks were successful
CI Code / Check spelling (pull_request) Successful in 15s
CI Code / Check coding style (pull_request) Successful in 23s
CI Code / Code Coverage (pull_request) Successful in 3m8s
CI Code / Linux (debian) (pull_request) Successful in 4m56s
CI Code / Linux (ubuntu) (pull_request) Successful in 8m5s
CI Code / Linux (arch) (pull_request) Successful in 14m29s
RFC 6120 §8.1.2.1: a stanza received over a c2s stream without a 'from'
attribute must be treated as coming from the server itself. The
on-connect disco#info handler passed the absent attribute as NULL into
connection_features_received(), where g_str_hash() dereferenced the NULL
key and crashed (remotely triggerable DoS on connect).

Substitute connection_get_domain() at both disco#info handler
boundaries, and make connection_features_received() and
connection_get_features() NULL-safe as defense in depth. Add a stabber
regression test answering the on-connect disco#info with a from-less
result.

Fixes #168
2026-07-27 13:01:21 +00:00
964f73d0ad fix(editor): follow live terminal size in external editor
All checks were successful
CI Code / Check spelling (pull_request) Successful in 14s
CI Code / Check coding style (pull_request) Successful in 22s
CI Code / Linux (debian) (pull_request) Successful in 4m24s
CI Code / Code Coverage (pull_request) Successful in 3m8s
CI Code / Linux (ubuntu) (pull_request) Successful in 8m41s
CI Code / Linux (arch) (pull_request) Successful in 13m2s
CI Code / Check spelling (push) Successful in 15s
CI Code / Check coding style (push) Successful in 23s
CI Code / Code Coverage (push) Successful in 3m13s
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 3m35s
CI Code / Linux (debian) (push) Successful in 5m0s
CI Code / Linux (ubuntu) (push) Successful in 5m5s
CI Code / Linux (arch) (push) Successful in 6m30s
The compose editor is spawned via fork+execvp and inherits profanity's
LINES/COLUMNS, which hold the size captured at startup and are never
refreshed on resize. A curses editor (nano, vim, ...) honors them over
the real window, so it renders at the launch-time size after a resize.

Drop LINES/COLUMNS from the child's environment before forking so its
curses falls back to ioctl(TIOCGWINSZ). Assembling the env in the parent
lets the child only reassign environ instead of calling unsetenv()
between fork and exec, keeping it clear of unsetenv()'s allocator work in
the multithreaded fork->exec window. profanity itself is unaffected.
2026-07-25 15:19:22 +00:00
2be16df905 ci(docker): add Docker publishing workflow and Dockerfile
All checks were successful
CI Code / Check spelling (pull_request) Successful in 13s
CI Code / Check coding style (pull_request) Successful in 24s
CI Code / Code Coverage (pull_request) Successful in 3m42s
CI Code / Linux (debian) (pull_request) Successful in 5m13s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m18s
CI Code / Linux (arch) (pull_request) Successful in 6m59s
CI Code / Check spelling (push) Successful in 13s
CI Code / Check coding style (push) Successful in 23s
CI Code / Code Coverage (push) Successful in 3m38s
Publish Docker image / Push Docker image to Docker Hub (push) Successful in 4m46s
CI Code / Linux (debian) (push) Successful in 5m26s
CI Code / Linux (ubuntu) (push) Successful in 5m33s
CI Code / Linux (arch) (push) Successful in 7m10s
Introduce GitHub Actions workflow for automated Docker image
publishing to Docker Hub, along with a dedicated Dockerfile in
the ci directory to streamline container builds.
2026-07-11 17:43:16 +00:00
c5dd0cf9df fix(xmpp): obfuscate client identity in protocol exchanges
All checks were successful
CI Code / Check spelling (pull_request) Successful in 12s
CI Code / Check coding style (pull_request) Successful in 24s
CI Code / Code Coverage (pull_request) Successful in 3m54s
CI Code / Linux (debian) (pull_request) Successful in 5m16s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m22s
CI Code / Linux (arch) (pull_request) Successful in 6m56s
CI Code / Check spelling (push) Successful in 13s
CI Code / Check coding style (push) Successful in 27s
CI Code / Code Coverage (push) Successful in 3m35s
CI Code / Linux (debian) (push) Successful in 5m41s
CI Code / Linux (ubuntu) (push) Successful in 5m52s
CI Code / Linux (arch) (push) Successful in 7m36s
Modify version responses to return a generic client name and omit
version strings. Drop the "profanity." prefix from dynamically
generated JID resources. Spoof the XEP-0115 capabilities node URI to
prevent service discovery/presence fingerprinting. These adjustments
reduce the client's attack surface by minimizing identifiable metadata.
2026-07-10 08:54:24 +00:00
b34faa2099 feat(caps): add cache clearing to /caps
Add /caps clear to wipe the local capabilities cache.
Implement autocomplete for new subcommand.
2026-07-10 08:12:56 +00:00
756d4e18cf fix(accounts): complete client.name to client.account_name rename
All checks were successful
CI Code / Check spelling (pull_request) Successful in 12s
CI Code / Check coding style (pull_request) Successful in 22s
CI Code / Code Coverage (pull_request) Successful in 3m21s
CI Code / Linux (debian) (pull_request) Successful in 5m12s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m18s
CI Code / Check spelling (push) Successful in 13s
CI Code / Check coding style (push) Successful in 23s
CI Code / Linux (ubuntu) (push) Successful in 4m43s
CI Code / Linux (debian) (push) Successful in 9m8s
CI Code / Linux (arch) (pull_request) Successful in 13m3s
CI Code / Code Coverage (push) Successful in 9m17s
CI Code / Linux (arch) (push) Successful in 13m4s
The previous rename of the account option key left "client.name" in
accounts_set_client() and accounts_clear_client(), preventing the
option from being set/cleared. Update both functions to use the
correct "client.account_name" key.

Note: existing configurations with the old key will not be migrated
automatically; a migration fix will follow.
2026-07-08 11:00:31 +00:00
28 changed files with 396 additions and 67 deletions

44
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: Publish Docker image
on:
push:
branches: [master]
release:
types: [published]
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v7
- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
with:
images: cproofdev/cproof
tags: |
type=raw,value=dev,enable={{is_default_branch}}
type=sha,prefix=nightly-,enable={{is_default_branch}}
type=ref,event=tag
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@2bd26e71295ee32cbf6a73510d165bf7232460f3
with:
context: .
file: ./ci/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

106
ci/Dockerfile Normal file
View File

@@ -0,0 +1,106 @@
# =============================================================================
# Stage 1: Build
# =============================================================================
FROM ubuntu:26.04 AS builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
autoconf \
automake \
autoconf-archive \
ca-certificates \
libtool \
pkg-config \
git \
wget \
libglib2.0-dev \
libcurl4-openssl-dev \
libsqlite3-dev \
libncurses-dev \
libreadline-dev \
libnotify-dev \
python3-dev \
python3 \
libgpgme-dev \
libotr5-dev \
libgtk-3-dev \
libgdk-pixbuf-2.0-dev \
libsignal-protocol-c-dev \
libgcrypt20-dev \
libqrencode-dev \
libxss-dev \
libcmocka-dev \
&& rm -rf /var/lib/apt/lists/*
# Build libstrophe from source (not available as Ubuntu package)
RUN git clone --depth 1 https://git.jabber.space/devs/libstrophe-gh-mirror.git /tmp/libstrophe \
&& cd /tmp/libstrophe \
&& autoreconf -i \
&& ./configure --prefix=/usr \
&& make -j$(nproc) \
&& make install \
&& rm -rf /tmp/libstrophe
WORKDIR /src
COPY . .
RUN ./bootstrap.sh \
&& ./configure \
--enable-notifications \
--enable-icons-and-clipboard \
--enable-otr \
--enable-pgp \
--enable-omemo \
--enable-plugins \
--enable-c-plugins \
--enable-python-plugins \
--with-xscreensaver \
--enable-omemo-qrcode \
--enable-gdk-pixbuf \
&& make -j$(nproc) \
&& make install
# =============================================================================
# Stage 2: Runtime (minimal)
# =============================================================================
FROM ubuntu:26.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
libglib2.0-0t64 \
libcurl4t64 \
libsqlite3-0 \
libncursesw6 \
libreadline8t64 \
libnotify4 \
python3 \
libpython3.14 \
libgpgme45 \
libotr5t64 \
libgtk-3-0t64 \
libgdk-pixbuf-2.0-0 \
libsignal-protocol-c2.3.2 \
libgcrypt20 \
libqrencode4 \
libxss1 \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
COPY --from=builder /usr/local/bin/profanity /usr/local/bin/profanity
COPY --from=builder /usr/local/lib/libprofanity* /usr/local/lib/
COPY --from=builder /usr/local/include/profapi.h /usr/local/include/
COPY --from=builder /usr/local/share/profanity /usr/local/share/profanity
COPY --from=builder /usr/lib/libstrophe.so* /usr/lib/
RUN ldconfig
RUN ldd /usr/local/bin/profanity | grep 'not found' && exit 1 || true
RUN mkdir -p /root/.config/profanity
ENTRYPOINT ["profanity"]

View File

@@ -42,6 +42,7 @@
#include "ai/ai_client.h"
static char* _caps_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _sub_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _notify_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _theme_autocomplete(ProfWin* window, const char* const input, gboolean previous);
@@ -151,6 +152,7 @@ static Autocomplete account_clear_ac;
static Autocomplete account_default_ac;
static Autocomplete account_status_ac;
static Autocomplete disco_ac;
static Autocomplete caps_subcommands_ac;
static Autocomplete wins_ac;
static Autocomplete roster_ac;
static Autocomplete roster_show_ac;
@@ -321,6 +323,7 @@ static Autocomplete* all_acs[] = {
&account_default_ac,
&account_status_ac,
&disco_ac,
&caps_subcommands_ac,
&wins_ac,
&roster_ac,
&roster_show_ac,
@@ -597,6 +600,8 @@ cmd_ac_init(void)
autocomplete_add(disco_ac, "info");
autocomplete_add(disco_ac, "items");
autocomplete_add(caps_subcommands_ac, "clear");
autocomplete_add(account_ac, "list");
autocomplete_add(account_ac, "show");
autocomplete_add(account_ac, "add");
@@ -1414,6 +1419,7 @@ cmd_ac_init(void)
g_hash_table_insert(ac_funcs, "/ban", _ban_autocomplete);
g_hash_table_insert(ac_funcs, "/blocked", _blocked_autocomplete);
g_hash_table_insert(ac_funcs, "/bookmark", _bookmark_autocomplete);
g_hash_table_insert(ac_funcs, "/caps", _caps_autocomplete);
g_hash_table_insert(ac_funcs, "/clear", _clear_autocomplete);
g_hash_table_insert(ac_funcs, "/close", _close_autocomplete);
g_hash_table_insert(ac_funcs, "/cmd", _adhoc_cmd_autocomplete);
@@ -1985,6 +1991,35 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
return NULL;
}
static char*
_caps_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
char* result = NULL;
result = autocomplete_param_with_ac(input, "/caps", caps_subcommands_ac, TRUE, previous);
if (result) {
return result;
}
if (window->type == WIN_MUC) {
ProfMucWin* mucwin = (ProfMucWin*)window;
assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
Autocomplete nick_ac = muc_roster_ac(mucwin->roomjid);
if (nick_ac) {
result = autocomplete_param_with_ac(input, "/caps", nick_ac, TRUE, previous);
}
} else if (connection_get_status() == JABBER_CONNECTED) {
result = autocomplete_param_with_func(input, "/caps", roster_contact_autocomplete, previous, NULL);
if (result) {
return result;
}
result = autocomplete_param_with_func(input, "/caps", roster_barejid_autocomplete, previous, NULL);
if (result) {
return result;
}
result = autocomplete_param_with_func(input, "/caps", roster_fulljid_autocomplete, previous, NULL);
}
return result;
}
static char*
_sub_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{

View File

@@ -427,17 +427,21 @@ static const struct cmd_t command_defs[] = {
CMD_TAG_GROUPCHAT)
CMD_SYN(
"/caps",
"/caps <fulljid>|<nick>")
"/caps <fulljid>|<nick>",
"/caps clear")
CMD_DESC(
"Find out a contacts, or room members client software capabilities. "
"If in private chat initiated from a chat room, no parameter is required.")
"If in private chat initiated from a chat room, no parameter is required. "
"Use clear to remove all cached capabilities.")
CMD_ARGS(
{ "<fulljid>", "If in the console or a chat window, the full JID for which you wish to see capabilities." },
{ "<nick>", "If in a chat room, nickname for which you wish to see capabilities." })
{ "<nick>", "If in a chat room, nickname for which you wish to see capabilities." },
{ "clear", "Clear all cached capabilities." })
CMD_EXAMPLES(
"/caps ran@cold.sea.org/laptop",
"/caps ran@cold.sea.org/phone",
"/caps aegir")
"/caps aegir",
"/caps clear")
},
{ CMD_PREAMBLE("/software",

View File

@@ -70,6 +70,7 @@
#include "xmpp/stanza.h"
#include "xmpp/vcard_funcs.h"
#include "xmpp/xmpp.h"
#include "xmpp/capabilities.h"
#ifdef HAVE_LIBOTR
#include "otr/otr.h"
@@ -3387,6 +3388,12 @@ cmd_caps(ProfWin* window, const char* const command, gchar** args)
return TRUE;
}
if (g_strcmp0(args[0], "clear") == 0) {
caps_cache_clear();
cons_show("Capabilities cache cleared.");
return TRUE;
}
switch (window->type) {
case WIN_MUC:
if (args[0]) {
@@ -3405,6 +3412,7 @@ cmd_caps(ProfWin* window, const char* const command, gchar** args)
break;
case WIN_CHAT:
case WIN_CONSOLE:
case WIN_XML:
if (args[0]) {
auto_jid Jid* jid = jid_create(args[0]);

View File

@@ -921,7 +921,7 @@ prof_date_time_format_iso8601(GDateTime* dt)
return ret;
}
/* build profanity version string.
/* build CProof version string.
* example: 0.13.1dev.master.69d8c1f9
*/
gchar*

View File

@@ -522,7 +522,7 @@ accounts_set_script_start(const char* const account_name, const char* const valu
void
accounts_set_client(const char* const account_name, const char* const value)
{
_accounts_set_string_option(account_name, "client.name", value);
_accounts_set_string_option(account_name, "client.account_name", value);
}
void
@@ -576,7 +576,7 @@ accounts_clear_script_start(const char* const account_name)
void
accounts_clear_client(const char* const account_name)
{
_accounts_clear_string_option(account_name, "client.name");
_accounts_clear_string_option(account_name, "client.account_name");
}
void

View File

@@ -2689,7 +2689,6 @@ _get_default_boolean(preference_t pref)
case PREF_STATUSBAR_SHOW_NUMBER:
case PREF_STATUSBAR_SHOW_READ:
case PREF_STATUSBAR_SHOW_DBBACKEND:
case PREF_REVEAL_OS:
case PREF_CORRECTION_ALLOW:
case PREF_RECEIPTS_SEND:
case PREF_CARBONS:
@@ -2704,6 +2703,7 @@ _get_default_boolean(preference_t pref)
case PREF_STROPHE_SM_RESEND:
case PREF_AUTOPING_WARNING:
return TRUE;
case PREF_REVEAL_OS:
case PREF_SPELLCHECK_ENABLE:
case PREF_PGP_PUBKEY_AUTOIMPORT:
case PREF_FORCE_ENCRYPTION:

View File

@@ -77,7 +77,7 @@ main(int argc, char** argv)
if (version == TRUE) {
auto_gchar gchar* prof_version = prof_get_version();
g_print("Profanity, version %s\n", prof_version);
g_print("CProof, version %s\n", prof_version);
// lets use fixed email instead of PACKAGE_BUGREPORT
g_print("Copyright (C) 2012 - 2019 James Booth <boothj5web@gmail.com>.\n");

View File

@@ -253,7 +253,7 @@ _init(char* log_level, char* config_file, char* log_file, char* theme_name)
log_stderr_init(PROF_LEVEL_ERROR);
auto_gchar gchar* prof_version = prof_get_version();
log_info("Starting Profanity (%s)…", prof_version);
log_info("Starting CProof (%s)…", prof_version);
chatlog_init();
accounts_load();

View File

@@ -25,6 +25,8 @@
#include "ui/ui.h"
#include "xmpp/xmpp.h"
extern char** environ;
typedef struct EditorContext
{
gchar* filename;
@@ -140,6 +142,22 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
GSource* sigchld_warmup = g_child_watch_source_new(getpid());
g_source_unref(sigchld_warmup);
// Build the editor's env without LINES/COLUMNS pre-fork, so the child only
// reassigns environ instead of calling unsetenv() between fork and exec.
// The editor's (n)curses then reads the live window via ioctl(TIOCGWINSZ).
gsize env_len = 0;
while (environ[env_len]) {
env_len++;
}
gchar** editor_env = g_new0(gchar*, env_len + 1);
gsize env_kept = 0;
for (gsize i = 0; i < env_len; i++) {
if (g_str_has_prefix(environ[i], "LINES=") || g_str_has_prefix(environ[i], "COLUMNS=")) {
continue;
}
editor_env[env_kept++] = environ[i];
}
pid_t pid = fork();
if (pid == -1) {
log_error("[Editor] Failed to fork: %s", strerror(errno));
@@ -148,12 +166,14 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
ui_resize();
cons_show_error("Failed to start editor: %s", strerror(errno));
g_strfreev(editor_argv);
g_free(ctx->filename);
g_free(editor_env);
g_free(ctx);
return TRUE;
} else if (pid == 0) {
// Child process: Inherits TTY from parent
environ = editor_env; // live TIOCGWINSZ size, not the inherited LINES/COLUMNS
// SIGTSTP=SIG_DFL lets vim's :stop / Ctrl-Z work; profanity catches
// the STOPPED state via editor_check_stopped() and drops to the shell.
signal(SIGINT, SIG_DFL);
@@ -170,6 +190,7 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
editor_pid = pid;
g_child_watch_add((GPid)pid, _editor_exit_cb, ctx);
g_strfreev(editor_argv);
g_free(editor_env); // array only; strings are borrowed from environ
return FALSE;
}

View File

@@ -2830,8 +2830,7 @@ cons_privacy_setting(void)
if (account->client) {
cons_show("Client name (/account set <account> clientid) : %s", account->client);
} else {
auto_gchar gchar* prof_version = prof_get_version();
cons_show("Client name (/account set <account> clientid) : Profanity %s", prof_version);
cons_show("Client name (/account set <account> clientid) : Pidgin");
}
if (account->max_sessions > 0) {
cons_show("Max sessions alarm (/account set <account> session_alarm) : %d", account->max_sessions);

View File

@@ -23,6 +23,7 @@
#include "plugins/plugins.h"
#include "config/files.h"
#include "config/preferences.h"
#include "xmpp/connection.h"
#include "xmpp/xmpp.h"
#include "xmpp/stanza.h"
#include "xmpp/form.h"
@@ -271,6 +272,19 @@ caps_map_jid_to_ver(const char* const jid, const char* const ver)
g_hash_table_insert(jid_to_ver, strdup(jid), strdup(ver));
}
void
caps_cache_clear(void)
{
g_hash_table_remove_all(jid_to_ver);
g_hash_table_remove_all(jid_to_caps);
if (caps_prof_keyfile.keyfile) {
g_key_file_free(caps_prof_keyfile.keyfile);
}
caps_prof_keyfile.keyfile = g_key_file_new();
cache = caps_prof_keyfile.keyfile;
save_keyfile(&caps_prof_keyfile);
}
gboolean
caps_cache_contains(const char* const ver)
{

View File

@@ -28,5 +28,6 @@ void caps_map_jid_to_ver(const char* const jid, const char* const ver);
gboolean caps_cache_contains(const char* const ver);
GList* caps_get_features(void);
char* caps_get_my_sha1(xmpp_ctx_t* const ctx);
void caps_cache_clear(void);
#endif

View File

@@ -749,11 +749,22 @@ connection_get_user(void)
return connection_get_jid()->localpart;
}
// NULL 'from' means the server (RFC 6120 §8.1.2.1)
static const char*
_get_from_via_jid(const char* const jid)
{
return jid ? jid : conn.domain;
}
void
connection_features_received(const char* const jid)
{
log_info("[CONNECTION] connection_features_received %s", jid);
if (g_hash_table_remove(conn.requested_features, jid) && g_hash_table_size(conn.requested_features) == 0) {
const char* key = _get_from_via_jid(jid);
if (!key) {
return;
}
if (g_hash_table_remove(conn.requested_features, key) && g_hash_table_size(conn.requested_features) == 0) {
sv_ev_connection_features_received();
}
}
@@ -761,7 +772,11 @@ connection_features_received(const char* const jid)
GHashTable*
connection_get_features(const char* const jid)
{
return g_hash_table_lookup(conn.features_by_jid, jid);
const char* key = _get_from_via_jid(jid);
if (!key || !conn.features_by_jid) {
return NULL;
}
return g_hash_table_lookup(conn.features_by_jid, key);
}
GList*

View File

@@ -1681,7 +1681,7 @@ _version_get_handler(xmpp_stanza_t* const stanza)
xmpp_stanza_t* name = xmpp_stanza_new(ctx);
xmpp_stanza_set_name(name, "name");
xmpp_stanza_t* name_txt = xmpp_stanza_new(ctx);
xmpp_stanza_set_text(name_txt, is_custom_client ? client : "Profanity");
xmpp_stanza_set_text(name_txt, is_custom_client ? client : "Pidgin");
xmpp_stanza_add_child(name, name_txt);
xmpp_stanza_add_child(query, name);
bool include_os = prefs_get_boolean(PREF_REVEAL_OS) && !is_custom_client;
@@ -1690,7 +1690,7 @@ _version_get_handler(xmpp_stanza_t* const stanza)
xmpp_stanza_t* version = xmpp_stanza_new(ctx);
xmpp_stanza_set_name(version, "version");
xmpp_stanza_t* version_txt = xmpp_stanza_new(ctx);
auto_gchar gchar* prof_version = prof_get_version();
auto_gchar gchar* prof_version = g_strdup("");
if (!is_custom_client) {
xmpp_stanza_set_text(version_txt, prof_version);
@@ -2314,6 +2314,7 @@ _disco_info_response_id_handler(xmpp_stanza_t* const stanza, void* const userdat
log_debug("Received disco#info response from: %s", from);
} else {
log_debug("Received disco#info response");
from = connection_get_domain(); // RFC 6120 §8.1.2.1: no 'from' means the server itself
}
// handle error responses
@@ -2397,6 +2398,7 @@ _disco_info_response_id_handler_onconnect(xmpp_stanza_t* const stanza, void* con
log_debug("Received disco#info response from: %s", from);
} else {
log_debug("Received disco#info response");
from = connection_get_domain(); // RFC 6120 §8.1.2.1: no 'from' means the server itself
}
// handle error responses

View File

@@ -280,9 +280,13 @@ jid_fulljid_or_barejid(Jid* jid)
gchar*
jid_random_resource(void)
{
auto_gchar gchar* rand = get_random_string(4);
gchar* rand = g_malloc0(29);
const gchar* digits = "0123456789";
gchar* result = g_strdup_printf("profanity.%s", rand);
for (int i = 0; i < 28; i++) {
rand[i] = digits[g_random_int_range(0, 10)];
}
rand[28] = '\0';
return result;
return rand;
}

View File

@@ -178,7 +178,7 @@ session_connect_with_details(const char* const jid, const char* const passwd, co
saved_details.auth_policy = NULL;
}
// use 'profanity' when no resourcepart in provided jid
// use random string when no resourcepart in provided jid
auto_jid Jid* jidp = jid_create(jid);
if (jidp->resourcepart == NULL) {
auto_gchar gchar* resource = jid_random_resource();

View File

@@ -32,6 +32,7 @@
static void _stanza_add_unique_id(xmpp_stanza_t* stanza);
static gchar* _stanza_create_sha1_hash(char* str);
static const char* _stanza_get_caps_node(const char* const client);
#if 0
xmpp_stanza_t*
@@ -930,23 +931,22 @@ stanza_create_caps_query_element(xmpp_ctx_t* ctx)
xmpp_stanza_t* identity = xmpp_stanza_new(ctx);
xmpp_stanza_set_name(identity, "identity");
xmpp_stanza_set_type(identity, "pc");
xmpp_stanza_set_attribute(identity, "category", "client");
ProfAccount* account = accounts_get_account(session_get_account_name());
gchar* client = account->client;
bool is_custom_client = client != NULL;
GString* name_str = g_string_new(is_custom_client ? client : "Profanity ");
if (!is_custom_client) {
xmpp_stanza_set_type(identity, "console");
auto_gchar gchar* prof_version = prof_get_version();
g_string_append(name_str, prof_version);
auto_gchar gchar* name = g_strdup(account->client ? account->client : "Pidgin");
if (account->client) {
gchar* space = strchr(name, ' ');
if (space) {
*space = '\0';
}
}
account_free(account);
xmpp_stanza_set_attribute(identity, "name", name_str->str);
g_string_free(name_str, TRUE);
xmpp_stanza_set_attribute(identity, "name", name);
xmpp_stanza_add_child(query, identity);
xmpp_stanza_release(identity);
@@ -1994,6 +1994,42 @@ stanza_attach_last_activity(xmpp_ctx_t* const ctx,
xmpp_stanza_release(query);
}
static const char*
_stanza_get_caps_node(const char* const client)
{
if (!client || g_strcmp0(client, "") == 0) {
return "http://pidgin.im";
}
auto_gchar gchar* lower = g_ascii_strdown(client, -1);
if (g_str_has_prefix(lower, "pidgin")) {
return "http://pidgin.im";
}
if (g_str_has_prefix(lower, "gajim")) {
return "https://gajim.org";
}
if (g_str_has_prefix(lower, "conversations")) {
return "https://conversations.im";
}
if (g_str_has_prefix(lower, "psi+")) {
return "http://psi-plus.com";
}
if (g_str_has_prefix(lower, "psi")) {
return "http://psi-im.org";
}
if (g_str_has_prefix(lower, "profanity")) {
return "http://profanity-im.github.io";
}
return "";
}
void
stanza_attach_caps(xmpp_ctx_t* const ctx, xmpp_stanza_t* const presence)
{
@@ -2002,9 +2038,13 @@ stanza_attach_caps(xmpp_ctx_t* const ctx, xmpp_stanza_t* const presence)
xmpp_stanza_set_ns(caps, STANZA_NS_CAPS);
xmpp_stanza_t* query = stanza_create_caps_query_element(ctx);
ProfAccount* account = accounts_get_account(session_get_account_name());
const char* node = _stanza_get_caps_node(account->client);
account_free(account);
char* sha1 = caps_get_my_sha1(ctx);
xmpp_stanza_set_attribute(caps, STANZA_ATTR_HASH, "sha-1");
xmpp_stanza_set_attribute(caps, STANZA_ATTR_NODE, "http://profanity-im.github.io");
xmpp_stanza_set_attribute(caps, STANZA_ATTR_NODE, node);
xmpp_stanza_set_attribute(caps, STANZA_ATTR_VER, sha1);
xmpp_stanza_add_child(presence, caps);
xmpp_stanza_release(caps);

View File

@@ -173,6 +173,7 @@ main(int argc, char* argv[])
PROF_FUNC_TEST(disco_info_without_name),
PROF_FUNC_TEST(disco_items_without_name),
PROF_FUNC_TEST(disco_info_service_unavailable),
PROF_FUNC_TEST(disco_info_result_no_from),
/* Roster management - add/remove/rename contacts */
PROF_FUNC_TEST(sends_new_item),

View File

@@ -601,11 +601,11 @@ prof_connect_with_roster(const char* roster)
assert_true(prof_output_regex(".+online.+ \\(priority 0\\)\\."));
expect_timeout = EXPECT_TIMEOUT_CONNECT;
// Wait for presence stanza to be sent (content-based, not ID-based)
// Wait for presence stanza to be sent
// Match the actual attribute order from stanza_attach_caps
assert_true(stbbr_received(
"<presence id=\"*\">"
"<c xmlns=\"http://jabber.org/protocol/caps\" hash=\"sha-1\" node=\"http://profanity-im.github.io\" ver=\"*\"/>"
"<c xmlns=\"http://jabber.org/protocol/caps\" hash=\"sha-1\" node=\"*\" ver=\"*\"/>"
"</presence>"));
}

View File

@@ -25,7 +25,7 @@ connect_jid_sends_presence_after_receiving_roster(void **state)
assert_true(stbbr_received(
"<presence id='*'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}

View File

@@ -396,6 +396,35 @@ disco_items_without_name(void **state)
prof_timeout_reset();
}
void
disco_info_result_no_from(void **state)
{
/*
* Test that a disco#info result without a 'from' attribute is treated as
* coming from the server itself (RFC 6120 §8.1.2.1). The on-connect
* disco#info handler used to crash on such responses (issue #168).
*/
stbbr_for_query("http://jabber.org/protocol/disco#info",
"<iq to='stabber@localhost/profanity' type='result'>"
"<query xmlns='http://jabber.org/protocol/disco#info'>"
"<identity category='server' type='im' name='NoFromServer'/>"
"<feature var='urn:xmpp:ping'/>"
"</query>"
"</iq>"
);
/* the on-connect disco#info gets the same from-less response */
prof_connect();
prof_input("/disco info");
prof_timeout(10);
/* client survived and attributed the response to the server */
assert_true(prof_output_exact("Service discovery info for localhost"));
assert_true(prof_output_regex("NoFromServer.*im.*server"));
prof_timeout_reset();
}
void
disco_info_service_unavailable(void **state)
{

View File

@@ -17,3 +17,4 @@ void disco_info_multiple_identities(void **state);
void disco_info_without_name(void **state);
void disco_items_without_name(void **state);
void disco_info_service_unavailable(void **state);
void disco_info_result_no_from(void **state);

View File

@@ -1289,7 +1289,7 @@ _join_muc(const char* room)
snprintf(presence, sizeof(presence),
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='%s/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' "
"node='http://profanity-im.github.io' ver='*'/>"
"node='*' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
"</x>"

View File

@@ -26,7 +26,7 @@ sends_room_join(void **state)
assert_true(stbbr_received(
"<presence id='*' to='testroom@conference.localhost/stabber'>"
"<x xmlns='http://jabber.org/protocol/muc'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -41,7 +41,7 @@ sends_room_join_with_nick(void **state)
assert_true(stbbr_received(
"<presence id='*' to='testroom@conference.localhost/testnick'>"
"<x xmlns='http://jabber.org/protocol/muc'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -58,7 +58,7 @@ sends_room_join_with_password(void **state)
"<x xmlns='http://jabber.org/protocol/muc'>"
"<password>testpassword</password>"
"</x>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -75,7 +75,7 @@ sends_room_join_with_nick_and_password(void **state)
"<x xmlns='http://jabber.org/protocol/muc'>"
"<password>testpassword</password>"
"</x>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -87,7 +87,7 @@ shows_role_and_affiliation_on_join(void **state)
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='*' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
"</x>"
@@ -107,7 +107,7 @@ shows_subject_on_join(void **state)
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='*' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
"</x>"
@@ -135,7 +135,7 @@ shows_history_message(void **state)
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='*' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
"</x>"
@@ -170,7 +170,7 @@ shows_occupant_join(void **state)
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='*' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
"</x>"
@@ -200,7 +200,7 @@ shows_message(void **state)
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='*' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
"</x>"
@@ -227,7 +227,7 @@ shows_me_message_from_occupant(void **state)
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='*' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
"</x>"
@@ -254,7 +254,7 @@ shows_me_message_from_self(void **state)
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='*' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
"</x>"
@@ -281,7 +281,7 @@ shows_all_messages_in_console_when_window_not_focussed(void **state)
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='*' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
"</x>"
@@ -322,7 +322,7 @@ shows_first_message_in_console_when_window_not_focussed(void **state)
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='*' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
"</x>"
@@ -369,7 +369,7 @@ shows_no_message_in_console_when_window_not_focussed(void **state)
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='*' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='participant' jid='stabber@localhost/profanity' affiliation='none'/>"
"</x>"
@@ -401,7 +401,7 @@ sends_affiliation_list_request(void **state)
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='*' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='moderator' jid='stabber@localhost/profanity' affiliation='owner'/>"
"</x>"
@@ -434,7 +434,7 @@ sends_kick_request(void **state)
stbbr_for_presence_to("testroom@conference.localhost/stabber",
"<presence id='*' lang='en' to='stabber@localhost/profanity' from='testroom@conference.localhost/stabber'>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='http://profanity-im.github.io' ver='*'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' node='*' ver='*'/>"
"<x xmlns='http://jabber.org/protocol/muc#user'>"
"<item role='moderator' jid='stabber@localhost/profanity' affiliation='admin'/>"
"</x>"

View File

@@ -19,7 +19,7 @@ presence_online(void **state)
assert_true(stbbr_received(
"<presence id='*'>"
"<status>online</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -36,7 +36,7 @@ presence_online_with_message(void **state)
assert_true(stbbr_received(
"<presence id='*'>"
"<status>Hi there</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -54,7 +54,7 @@ presence_away(void **state)
"<presence id='*'>"
"<show>away</show>"
"<status>away</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -72,7 +72,7 @@ presence_away_with_message(void **state)
"<presence id='*'>"
"<show>away</show>"
"<status>I'm not here for a bit</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -90,7 +90,7 @@ presence_xa(void **state)
"<presence id='*'>"
"<show>xa</show>"
"<status>xa</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -108,7 +108,7 @@ presence_xa_with_message(void **state)
"<presence id='*'>"
"<show>xa</show>"
"<status>Gone to the shops</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -126,7 +126,7 @@ presence_dnd(void **state)
"<presence id='*'>"
"<show>dnd</show>"
"<status>dnd</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -144,7 +144,7 @@ presence_dnd_with_message(void **state)
"<presence id='*'>"
"<show>dnd</show>"
"<status>Working</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -162,7 +162,7 @@ presence_chat(void **state)
"<presence id='*'>"
"<show>chat</show>"
"<status>chat</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -180,7 +180,7 @@ presence_chat_with_message(void **state)
"<presence id='*'>"
"<show>chat</show>"
"<status>Free to talk</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -197,7 +197,7 @@ presence_set_priority(void **state)
assert_true(stbbr_received(
"<presence id='*'>"
"<priority>25</priority>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -212,7 +212,7 @@ presence_includes_priority(void **state)
assert_true(stbbr_received(
"<presence id='*'>"
"<priority>25</priority>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
@@ -223,7 +223,7 @@ presence_includes_priority(void **state)
"<priority>25</priority>"
"<show>chat</show>"
"<status>Free to talk</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}
@@ -239,7 +239,7 @@ presence_keeps_status(void **state)
"<presence id='*'>"
"<show>chat</show>"
"<status>Free to talk</status>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
@@ -250,7 +250,7 @@ presence_keeps_status(void **state)
"<show>chat</show>"
"<status>Free to talk</status>"
"<priority>25</priority>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='http://profanity-im.github.io'/>"
"<c hash='sha-1' xmlns='http://jabber.org/protocol/caps' ver='*' node='*'/>"
"</presence>"
));
}

View File

@@ -500,6 +500,11 @@ caps_jid_has_feature(const char* const jid, const char* const feature)
return FALSE;
}
void
caps_cache_clear(void)
{
}
gboolean
bookmark_add(const char* jid, const char* nick, const char* password, const char* autojoin_str, const char* name)
{