It is useful in some applications to get a snapshot of the stream management state for storage outside the process, in order to recover from crashes and other things. The get_sm_state already present is not suitable for this because it returns a live object tied in to the current context and such, and containing much unneeded internal-api data. Introduce xmpp_sm_state_set_callback which is called every time the state changes with a serialized state, and a dual xmpp_sm_state_restore which takes in this serialized state and sets up a new sm_state based on that. The serialization is considered opaque from the PoV of the API, but is based on CBOR to facilitate easy debugging.
1765 lines
56 KiB
C
1765 lines
56 KiB
C
/* SPDX-License-Identifier: MIT OR GPL-3.0-only */
|
|
/* auth.c
|
|
** strophe XMPP client library -- auth functions and handlers
|
|
**
|
|
** Copyright (C) 2005-2009 Collecta, Inc.
|
|
**
|
|
** This software is provided AS-IS with no warranty, either express or
|
|
** implied.
|
|
**
|
|
** This program is dual licensed under the MIT or GPLv3 licenses.
|
|
*/
|
|
|
|
/** @file
|
|
* Authentication function and handlers.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "strophe.h"
|
|
#include "common.h"
|
|
#include "sasl.h"
|
|
#include "sha1.h"
|
|
|
|
#ifdef _MSC_VER
|
|
#define strcasecmp _stricmp
|
|
#endif
|
|
|
|
/* TODO: these should configurable at runtime on a per connection basis */
|
|
|
|
#ifndef FEATURES_TIMEOUT
|
|
/** @def FEATURES_TIMEOUT
|
|
* Time to wait for <stream:features/> stanza.
|
|
*/
|
|
#define FEATURES_TIMEOUT 15000 /* 15 seconds */
|
|
#endif
|
|
#ifndef BIND_TIMEOUT
|
|
/** @def BIND_TIMEOUT
|
|
* Time to wait for <bind/> stanza reply.
|
|
*/
|
|
#define BIND_TIMEOUT 15000 /* 15 seconds */
|
|
#endif
|
|
#ifndef SESSION_TIMEOUT
|
|
/** @def SESSION_TIMEOUT
|
|
* Time to wait for <session/> stanza reply.
|
|
*/
|
|
#define SESSION_TIMEOUT 15000 /* 15 seconds */
|
|
#endif
|
|
#ifndef LEGACY_TIMEOUT
|
|
/** @def LEGACY_TIMEOUT
|
|
* Time to wait for legacy authentication to complete.
|
|
*/
|
|
#define LEGACY_TIMEOUT 15000 /* 15 seconds */
|
|
#endif
|
|
#ifndef HANDSHAKE_TIMEOUT
|
|
/** @def HANDSHAKE_TIMEOUT
|
|
* Time to wait for component authentication to complete
|
|
*/
|
|
#define HANDSHAKE_TIMEOUT 15000 /* 15 seconds */
|
|
#endif
|
|
|
|
static void _auth(xmpp_conn_t *conn);
|
|
static void _auth_legacy(xmpp_conn_t *conn);
|
|
static void _handle_open_compress(xmpp_conn_t *conn);
|
|
static void _handle_open_sasl(xmpp_conn_t *conn);
|
|
static void _handle_open_tls(xmpp_conn_t *conn);
|
|
|
|
static int _handle_component_auth(xmpp_conn_t *conn);
|
|
static int _handle_component_hs_response(xmpp_conn_t *conn,
|
|
xmpp_stanza_t *stanza,
|
|
void *userdata);
|
|
|
|
static int
|
|
_handle_features_sasl(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata);
|
|
static int _handle_features_compress(xmpp_conn_t *conn,
|
|
xmpp_stanza_t *stanza,
|
|
void *userdata);
|
|
static int
|
|
_handle_sasl_result(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata);
|
|
static int _handle_digestmd5_challenge(xmpp_conn_t *conn,
|
|
xmpp_stanza_t *stanza,
|
|
void *userdata);
|
|
static int _handle_digestmd5_rspauth(xmpp_conn_t *conn,
|
|
xmpp_stanza_t *stanza,
|
|
void *userdata);
|
|
static int _handle_scram_challenge(xmpp_conn_t *conn,
|
|
xmpp_stanza_t *stanza,
|
|
void *userdata);
|
|
struct scram_user_data;
|
|
static int _make_scram_init_msg(struct scram_user_data *scram);
|
|
|
|
static int _handle_missing_features_sasl(xmpp_conn_t *conn, void *userdata);
|
|
static int _handle_missing_bind(xmpp_conn_t *conn, void *userdata);
|
|
static int
|
|
_handle_bind(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata);
|
|
static int
|
|
_handle_session(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata);
|
|
static int _handle_missing_session(xmpp_conn_t *conn, void *userdata);
|
|
static int _handle_missing_handshake(xmpp_conn_t *conn, void *userdata);
|
|
static int _handle_sm(xmpp_conn_t *const conn,
|
|
xmpp_stanza_t *const stanza,
|
|
void *const userdata);
|
|
|
|
/* stream:error handler */
|
|
static int
|
|
_handle_error(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata)
|
|
{
|
|
xmpp_stanza_t *child;
|
|
const char *name;
|
|
|
|
UNUSED(userdata);
|
|
|
|
/* free old stream error if it's still there */
|
|
if (conn->stream_error) {
|
|
xmpp_stanza_release(conn->stream_error->stanza);
|
|
if (conn->stream_error->text)
|
|
strophe_free(conn->ctx, conn->stream_error->text);
|
|
strophe_free(conn->ctx, conn->stream_error);
|
|
}
|
|
|
|
/* create stream error structure */
|
|
conn->stream_error = (xmpp_stream_error_t *)strophe_alloc(
|
|
conn->ctx, sizeof(xmpp_stream_error_t));
|
|
|
|
conn->stream_error->text = NULL;
|
|
conn->stream_error->type = XMPP_SE_UNDEFINED_CONDITION;
|
|
|
|
if (conn->stream_error) {
|
|
child = xmpp_stanza_get_children(stanza);
|
|
do {
|
|
const char *ns = NULL;
|
|
|
|
if (child) {
|
|
ns = xmpp_stanza_get_ns(child);
|
|
}
|
|
|
|
if (ns && strcmp(ns, XMPP_NS_STREAMS_IETF) == 0) {
|
|
name = xmpp_stanza_get_name(child);
|
|
if (strcmp(name, "text") == 0) {
|
|
if (conn->stream_error->text)
|
|
strophe_free(conn->ctx, conn->stream_error->text);
|
|
conn->stream_error->text = xmpp_stanza_get_text(child);
|
|
} else if (strcmp(name, "bad-format") == 0)
|
|
conn->stream_error->type = XMPP_SE_BAD_FORMAT;
|
|
else if (strcmp(name, "bad-namespace-prefix") == 0)
|
|
conn->stream_error->type = XMPP_SE_BAD_NS_PREFIX;
|
|
else if (strcmp(name, "conflict") == 0)
|
|
conn->stream_error->type = XMPP_SE_CONFLICT;
|
|
else if (strcmp(name, "connection-timeout") == 0)
|
|
conn->stream_error->type = XMPP_SE_CONN_TIMEOUT;
|
|
else if (strcmp(name, "host-gone") == 0)
|
|
conn->stream_error->type = XMPP_SE_HOST_GONE;
|
|
else if (strcmp(name, "host-unknown") == 0)
|
|
conn->stream_error->type = XMPP_SE_HOST_UNKNOWN;
|
|
else if (strcmp(name, "improper-addressing") == 0)
|
|
conn->stream_error->type = XMPP_SE_IMPROPER_ADDR;
|
|
else if (strcmp(name, "internal-server-error") == 0)
|
|
conn->stream_error->type = XMPP_SE_INTERNAL_SERVER_ERROR;
|
|
else if (strcmp(name, "invalid-from") == 0)
|
|
conn->stream_error->type = XMPP_SE_INVALID_FROM;
|
|
else if (strcmp(name, "invalid-id") == 0)
|
|
conn->stream_error->type = XMPP_SE_INVALID_ID;
|
|
else if (strcmp(name, "invalid-namespace") == 0)
|
|
conn->stream_error->type = XMPP_SE_INVALID_NS;
|
|
else if (strcmp(name, "invalid-xml") == 0)
|
|
conn->stream_error->type = XMPP_SE_INVALID_XML;
|
|
else if (strcmp(name, "not-authorized") == 0)
|
|
conn->stream_error->type = XMPP_SE_NOT_AUTHORIZED;
|
|
else if (strcmp(name, "policy-violation") == 0)
|
|
conn->stream_error->type = XMPP_SE_POLICY_VIOLATION;
|
|
else if (strcmp(name, "remote-connection-failed") == 0)
|
|
conn->stream_error->type = XMPP_SE_REMOTE_CONN_FAILED;
|
|
else if (strcmp(name, "resource-constraint") == 0)
|
|
conn->stream_error->type = XMPP_SE_RESOURCE_CONSTRAINT;
|
|
else if (strcmp(name, "restricted-xml") == 0)
|
|
conn->stream_error->type = XMPP_SE_RESTRICTED_XML;
|
|
else if (strcmp(name, "see-other-host") == 0)
|
|
conn->stream_error->type = XMPP_SE_SEE_OTHER_HOST;
|
|
else if (strcmp(name, "system-shutdown") == 0)
|
|
conn->stream_error->type = XMPP_SE_SYSTEM_SHUTDOWN;
|
|
else if (strcmp(name, "undefined-condition") == 0)
|
|
conn->stream_error->type = XMPP_SE_UNDEFINED_CONDITION;
|
|
else if (strcmp(name, "unsupported-encoding") == 0)
|
|
conn->stream_error->type = XMPP_SE_UNSUPPORTED_ENCODING;
|
|
else if (strcmp(name, "unsupported-stanza-type") == 0)
|
|
conn->stream_error->type = XMPP_SE_UNSUPPORTED_STANZA_TYPE;
|
|
else if (strcmp(name, "unsupported-version") == 0)
|
|
conn->stream_error->type = XMPP_SE_UNSUPPORTED_VERSION;
|
|
else if (strcmp(name, "xml-not-well-formed") == 0)
|
|
conn->stream_error->type = XMPP_SE_XML_NOT_WELL_FORMED;
|
|
}
|
|
} while ((child = xmpp_stanza_get_next(child)));
|
|
|
|
conn->stream_error->stanza = xmpp_stanza_clone(stanza);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* stream:features handlers */
|
|
static int _handle_missing_features(xmpp_conn_t *conn, void *userdata)
|
|
{
|
|
UNUSED(userdata);
|
|
|
|
strophe_debug(conn->ctx, "xmpp", "didn't get stream features");
|
|
|
|
/* legacy auth will be attempted */
|
|
_auth(conn);
|
|
|
|
return 0;
|
|
}
|
|
|
|
typedef void (*text_handler)(xmpp_conn_t *conn, const char *text);
|
|
static void _foreach_child(xmpp_conn_t *conn,
|
|
xmpp_stanza_t *parent,
|
|
const char *name,
|
|
text_handler hndl)
|
|
{
|
|
xmpp_stanza_t *children;
|
|
for (children = xmpp_stanza_get_children(parent); children;
|
|
children = xmpp_stanza_get_next(children)) {
|
|
const char *child_name = xmpp_stanza_get_name(children);
|
|
if (child_name && strcmp(child_name, name) == 0) {
|
|
char *text = xmpp_stanza_get_text(children);
|
|
if (text == NULL)
|
|
continue;
|
|
|
|
hndl(conn, text);
|
|
|
|
strophe_free(conn->ctx, text);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void _handle_sasl_children(xmpp_conn_t *conn, const char *text)
|
|
{
|
|
if (strcasecmp(text, "PLAIN") == 0) {
|
|
conn->sasl_support |= SASL_MASK_PLAIN;
|
|
} else if (strcasecmp(text, "EXTERNAL") == 0 &&
|
|
(conn->tls_client_cert || conn->tls_client_key)) {
|
|
conn->sasl_support |= SASL_MASK_EXTERNAL;
|
|
} else if (strcasecmp(text, "DIGEST-MD5") == 0) {
|
|
conn->sasl_support |= SASL_MASK_DIGESTMD5;
|
|
} else if (strcasecmp(text, "ANONYMOUS") == 0) {
|
|
conn->sasl_support |= SASL_MASK_ANONYMOUS;
|
|
} else {
|
|
size_t n;
|
|
for (n = 0; n < scram_algs_num; ++n) {
|
|
if (strcasecmp(text, scram_algs[n]->scram_name) == 0) {
|
|
conn->sasl_support |= scram_algs[n]->mask;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static int
|
|
_handle_features(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata)
|
|
{
|
|
xmpp_stanza_t *child;
|
|
|
|
UNUSED(userdata);
|
|
|
|
/* remove the handler that detects missing stream:features */
|
|
xmpp_timed_handler_delete(conn, _handle_missing_features);
|
|
|
|
/* check for TLS */
|
|
if (!conn->secured) {
|
|
if (!conn->tls_disabled) {
|
|
if (xmpp_stanza_get_child_by_name_and_ns(stanza, "starttls",
|
|
XMPP_NS_TLS)) {
|
|
conn->tls_support = 1;
|
|
}
|
|
} else {
|
|
conn->tls_support = 0;
|
|
}
|
|
}
|
|
|
|
/* check for SASL */
|
|
child = xmpp_stanza_get_child_by_name_and_ns(stanza, "mechanisms",
|
|
XMPP_NS_SASL);
|
|
if (child) {
|
|
_foreach_child(conn, child, "mechanism", _handle_sasl_children);
|
|
}
|
|
|
|
/* Disable PLAIN when other secure mechanisms are supported */
|
|
if (conn->sasl_support & ~(SASL_MASK_PLAIN | SASL_MASK_ANONYMOUS))
|
|
conn->sasl_support &= ~SASL_MASK_PLAIN;
|
|
|
|
_auth(conn);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* returns the correct auth id for a component or a client.
|
|
* returned string must be freed by caller */
|
|
static char *_get_authid(xmpp_conn_t *conn)
|
|
{
|
|
char *authid = NULL;
|
|
|
|
if (conn->type == XMPP_CLIENT) {
|
|
/* authid is the node portion of jid */
|
|
if (!conn->jid)
|
|
return NULL;
|
|
authid = xmpp_jid_node(conn->ctx, conn->jid);
|
|
}
|
|
|
|
return authid;
|
|
}
|
|
|
|
static int _handle_proceedtls_default(xmpp_conn_t *conn,
|
|
xmpp_stanza_t *stanza,
|
|
void *userdata)
|
|
{
|
|
const char *name;
|
|
|
|
UNUSED(userdata);
|
|
|
|
name = xmpp_stanza_get_name(stanza);
|
|
strophe_debug(conn->ctx, "xmpp", "handle proceedtls called for %s", name);
|
|
|
|
if (strcmp(name, "proceed") == 0) {
|
|
strophe_debug(conn->ctx, "xmpp", "proceeding with TLS");
|
|
|
|
if (conn_tls_start(conn) == 0) {
|
|
conn_prepare_reset(conn, _handle_open_tls);
|
|
conn_open_stream(conn);
|
|
} else {
|
|
/* failed tls spoils the connection, so disconnect */
|
|
xmpp_disconnect(conn);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
_handle_sasl_result(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata)
|
|
{
|
|
const char *name;
|
|
|
|
name = xmpp_stanza_get_name(stanza);
|
|
|
|
/* the server should send a <success> or <failure> stanza */
|
|
if (strcmp(name, "failure") == 0) {
|
|
strophe_debug(conn->ctx, "xmpp", "SASL %s auth failed",
|
|
(char *)userdata);
|
|
|
|
/* fall back to next auth method */
|
|
_auth(conn);
|
|
} else if (strcmp(name, "success") == 0) {
|
|
/* SASL auth successful, we need to restart the stream */
|
|
strophe_debug(conn->ctx, "xmpp", "SASL %s auth successful",
|
|
(char *)userdata);
|
|
|
|
/* reset parser */
|
|
conn_prepare_reset(conn, conn->compression.allowed
|
|
? _handle_open_compress
|
|
: _handle_open_sasl);
|
|
|
|
/* send stream tag */
|
|
conn_open_stream(conn);
|
|
} else {
|
|
/* got unexpected reply */
|
|
strophe_error(conn->ctx, "xmpp",
|
|
"Got unexpected reply to SASL %s authentication.",
|
|
(char *)userdata);
|
|
xmpp_disconnect(conn);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* handle the challenge phase of digest auth */
|
|
static int _handle_digestmd5_challenge(xmpp_conn_t *conn,
|
|
xmpp_stanza_t *stanza,
|
|
void *userdata)
|
|
{
|
|
char *text;
|
|
char *response;
|
|
xmpp_stanza_t *auth, *authdata;
|
|
const char *name;
|
|
|
|
UNUSED(userdata);
|
|
|
|
name = xmpp_stanza_get_name(stanza);
|
|
strophe_debug(conn->ctx, "xmpp",
|
|
"handle digest-md5 (challenge) called for %s", name);
|
|
|
|
if (strcmp(name, "challenge") == 0) {
|
|
text = xmpp_stanza_get_text(stanza);
|
|
response = sasl_digest_md5(conn->ctx, text, conn->jid, conn->pass);
|
|
if (!response) {
|
|
disconnect_mem_error(conn);
|
|
return 0;
|
|
}
|
|
strophe_free(conn->ctx, text);
|
|
|
|
auth = xmpp_stanza_new(conn->ctx);
|
|
if (!auth) {
|
|
disconnect_mem_error(conn);
|
|
return 0;
|
|
}
|
|
xmpp_stanza_set_name(auth, "response");
|
|
xmpp_stanza_set_ns(auth, XMPP_NS_SASL);
|
|
|
|
authdata = xmpp_stanza_new(conn->ctx);
|
|
if (!authdata) {
|
|
disconnect_mem_error(conn);
|
|
return 0;
|
|
}
|
|
|
|
xmpp_stanza_set_text(authdata, response);
|
|
strophe_free(conn->ctx, response);
|
|
|
|
xmpp_stanza_add_child_ex(auth, authdata, 0);
|
|
|
|
handler_add(conn, _handle_digestmd5_rspauth, XMPP_NS_SASL, NULL, NULL,
|
|
NULL);
|
|
|
|
send_stanza(conn, auth, XMPP_QUEUE_STROPHE);
|
|
|
|
} else {
|
|
return _handle_sasl_result(conn, stanza, "DIGEST-MD5");
|
|
}
|
|
|
|
/* remove ourselves */
|
|
return 0;
|
|
}
|
|
|
|
/* handle the rspauth phase of digest auth */
|
|
static int _handle_digestmd5_rspauth(xmpp_conn_t *conn,
|
|
xmpp_stanza_t *stanza,
|
|
void *userdata)
|
|
{
|
|
xmpp_stanza_t *auth;
|
|
const char *name;
|
|
|
|
UNUSED(userdata);
|
|
|
|
name = xmpp_stanza_get_name(stanza);
|
|
strophe_debug(conn->ctx, "xmpp",
|
|
"handle digest-md5 (rspauth) called for %s", name);
|
|
|
|
if (strcmp(name, "challenge") == 0) {
|
|
/* assume it's an rspauth response */
|
|
auth = xmpp_stanza_new(conn->ctx);
|
|
if (!auth) {
|
|
disconnect_mem_error(conn);
|
|
return 0;
|
|
}
|
|
xmpp_stanza_set_name(auth, "response");
|
|
xmpp_stanza_set_ns(auth, XMPP_NS_SASL);
|
|
send_stanza(conn, auth, XMPP_QUEUE_STROPHE);
|
|
} else {
|
|
return _handle_sasl_result(conn, stanza, "DIGEST-MD5");
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
struct scram_user_data {
|
|
xmpp_conn_t *conn;
|
|
int sasl_plus;
|
|
char *scram_init;
|
|
char *channel_binding;
|
|
const char *first_bare;
|
|
const struct hash_alg *alg;
|
|
};
|
|
|
|
/* handle the challenge phase of SCRAM-SHA-1 auth */
|
|
static int _handle_scram_challenge(xmpp_conn_t *conn,
|
|
xmpp_stanza_t *stanza,
|
|
void *userdata)
|
|
{
|
|
char *text;
|
|
char *response;
|
|
xmpp_stanza_t *auth;
|
|
xmpp_stanza_t *authdata;
|
|
const char *name;
|
|
char *challenge;
|
|
struct scram_user_data *scram_ctx = (struct scram_user_data *)userdata;
|
|
int rc;
|
|
|
|
name = xmpp_stanza_get_name(stanza);
|
|
strophe_debug(conn->ctx, "xmpp", "handle %s (challenge) called for %s",
|
|
scram_ctx->alg->scram_name, name);
|
|
|
|
if (strcmp(name, "challenge") == 0) {
|
|
text = xmpp_stanza_get_text(stanza);
|
|
if (!text)
|
|
goto err;
|
|
|
|
challenge = xmpp_base64_decode_str(conn->ctx, text, strlen(text));
|
|
strophe_free(conn->ctx, text);
|
|
if (!challenge)
|
|
goto err;
|
|
|
|
response =
|
|
sasl_scram(conn->ctx, scram_ctx->alg, scram_ctx->channel_binding,
|
|
challenge, scram_ctx->first_bare, conn->jid, conn->pass);
|
|
strophe_free(conn->ctx, challenge);
|
|
if (!response)
|
|
goto err;
|
|
|
|
auth = xmpp_stanza_new(conn->ctx);
|
|
if (!auth)
|
|
goto err_free_response;
|
|
xmpp_stanza_set_name(auth, "response");
|
|
xmpp_stanza_set_ns(auth, XMPP_NS_SASL);
|
|
|
|
authdata = xmpp_stanza_new(conn->ctx);
|
|
if (!authdata)
|
|
goto err_release_auth;
|
|
xmpp_stanza_set_text(authdata, response);
|
|
strophe_free(conn->ctx, response);
|
|
|
|
xmpp_stanza_add_child_ex(auth, authdata, 0);
|
|
|
|
send_stanza(conn, auth, XMPP_QUEUE_STROPHE);
|
|
|
|
rc = 1; /* Keep handler */
|
|
} else {
|
|
/*
|
|
* Free scram_ctx after calling _handle_sasl_result(). If authentication
|
|
* fails, we want to try other mechanism which may be different SCRAM
|
|
* mechanism. If we freed scram_ctx before the function, _auth() would
|
|
* be able to allocate new scram_ctx object with the same address and
|
|
* handler_add() would consider new SCRAM handler as duplicate, because
|
|
* current handler is not removed yet. As result, libstrophe wouldn't
|
|
* handle incoming challenge stanza.
|
|
*/
|
|
rc = _handle_sasl_result(conn, stanza,
|
|
(void *)scram_ctx->alg->scram_name);
|
|
strophe_free_and_null(conn->ctx, scram_ctx->channel_binding);
|
|
strophe_free_and_null(conn->ctx, scram_ctx->scram_init);
|
|
strophe_free(conn->ctx, scram_ctx);
|
|
}
|
|
|
|
return rc;
|
|
|
|
err_release_auth:
|
|
xmpp_stanza_release(auth);
|
|
err_free_response:
|
|
strophe_free(conn->ctx, response);
|
|
err:
|
|
strophe_free_and_null(conn->ctx, scram_ctx->channel_binding);
|
|
strophe_free_and_null(conn->ctx, scram_ctx->scram_init);
|
|
strophe_free(conn->ctx, scram_ctx);
|
|
disconnect_mem_error(conn);
|
|
return 0;
|
|
}
|
|
|
|
static int _make_scram_init_msg(struct scram_user_data *scram)
|
|
{
|
|
xmpp_conn_t *conn = scram->conn;
|
|
xmpp_ctx_t *ctx = conn->ctx;
|
|
const void *binding_data;
|
|
const char *binding_type;
|
|
char *node, *message;
|
|
size_t message_len, binding_type_len = 0, binding_data_len;
|
|
int l, is_secured = xmpp_conn_is_secured(conn);
|
|
/* This buffer must be able to hold:
|
|
* "p=<10 bytes binding type>,,<36 bytes binding data>"
|
|
* + alignment */
|
|
char buf[56];
|
|
|
|
if (scram->sasl_plus) {
|
|
if (!is_secured) {
|
|
strophe_error(
|
|
ctx, "xmpp",
|
|
"SASL: Server requested a -PLUS variant to authenticate, "
|
|
"but the connection is not secured. This is an error on "
|
|
"the server side we can't do anything about.");
|
|
return -1;
|
|
}
|
|
if (tls_init_channel_binding(conn->tls, &binding_type,
|
|
&binding_type_len)) {
|
|
return -1;
|
|
}
|
|
/* directly account for the '=' char in 'p=<binding-type>' */
|
|
binding_type_len += 1;
|
|
}
|
|
|
|
node = xmpp_jid_node(ctx, conn->jid);
|
|
if (!node) {
|
|
return -1;
|
|
}
|
|
/* 32 bytes nonce is enough */
|
|
xmpp_rand_nonce(ctx->rand, buf, 33);
|
|
message_len = strlen(node) + strlen(buf) + 8 + binding_type_len + 1;
|
|
message = strophe_alloc(ctx, message_len);
|
|
if (!message) {
|
|
goto err_node;
|
|
}
|
|
/* increase length to account for 'y,,', 'n,,' or 'p,,'.
|
|
* In the 'p' case the '=' sign has already been accounted for above.
|
|
*/
|
|
binding_type_len += 3;
|
|
if (scram->sasl_plus) {
|
|
l = strophe_snprintf(message, message_len, "p=%s,,n=%s,r=%s",
|
|
binding_type, node, buf);
|
|
} else {
|
|
l = strophe_snprintf(message, message_len, "%c,,n=%s,r=%s",
|
|
is_secured ? 'y' : 'n', node, buf);
|
|
}
|
|
if (l < 0 || (size_t)l >= message_len) {
|
|
goto err_msg;
|
|
}
|
|
if (binding_type_len > sizeof(buf)) {
|
|
goto err_msg;
|
|
}
|
|
/* Make `first_bare` point to the 'n' of 'n=<node>' of the
|
|
* client-first-message */
|
|
scram->first_bare = message + binding_type_len;
|
|
memcpy(buf, message, binding_type_len);
|
|
if (scram->sasl_plus) {
|
|
binding_data =
|
|
tls_get_channel_binding_data(conn->tls, &binding_data_len);
|
|
if (!binding_data) {
|
|
goto err_msg;
|
|
}
|
|
if (binding_data_len > sizeof(buf) - binding_type_len) {
|
|
strophe_error(ctx, "xmpp", "Channel binding data is too long (%zu)",
|
|
binding_data_len);
|
|
goto err_msg;
|
|
}
|
|
memcpy(&buf[binding_type_len], binding_data, binding_data_len);
|
|
binding_type_len += binding_data_len;
|
|
}
|
|
scram->channel_binding =
|
|
xmpp_base64_encode(ctx, (void *)buf, binding_type_len);
|
|
memset(buf, 0, binding_type_len);
|
|
strophe_free(ctx, node);
|
|
scram->scram_init = message;
|
|
|
|
return 0;
|
|
|
|
err_msg:
|
|
strophe_free(ctx, message);
|
|
err_node:
|
|
strophe_free(ctx, node);
|
|
return -1;
|
|
}
|
|
|
|
static xmpp_stanza_t *_make_starttls(xmpp_conn_t *conn)
|
|
{
|
|
xmpp_stanza_t *starttls;
|
|
|
|
/* build start stanza */
|
|
starttls = xmpp_stanza_new(conn->ctx);
|
|
if (starttls) {
|
|
xmpp_stanza_set_name(starttls, "starttls");
|
|
xmpp_stanza_set_ns(starttls, XMPP_NS_TLS);
|
|
}
|
|
|
|
return starttls;
|
|
}
|
|
|
|
static xmpp_stanza_t *_make_sasl_auth(xmpp_conn_t *conn, const char *mechanism)
|
|
{
|
|
xmpp_stanza_t *auth;
|
|
|
|
/* build auth stanza */
|
|
auth = xmpp_stanza_new(conn->ctx);
|
|
if (auth) {
|
|
xmpp_stanza_set_name(auth, "auth");
|
|
xmpp_stanza_set_ns(auth, XMPP_NS_SASL);
|
|
xmpp_stanza_set_attribute(auth, "mechanism", mechanism);
|
|
}
|
|
|
|
return auth;
|
|
}
|
|
|
|
/* authenticate the connection
|
|
* this may get called multiple times. if any auth method fails,
|
|
* this will get called again until one auth method succeeds or every
|
|
* method fails
|
|
*/
|
|
static void _auth(xmpp_conn_t *conn)
|
|
{
|
|
xmpp_stanza_t *auth;
|
|
xmpp_stanza_t *authdata;
|
|
struct scram_user_data *scram_ctx;
|
|
char *authid;
|
|
char *str;
|
|
int anonjid;
|
|
|
|
/* if there is no node in conn->jid, we assume anonymous connect */
|
|
str = xmpp_jid_node(conn->ctx, conn->jid);
|
|
if (str == NULL) {
|
|
anonjid = 1;
|
|
} else {
|
|
strophe_free(conn->ctx, str);
|
|
anonjid = 0;
|
|
}
|
|
|
|
if (conn->tls_support) {
|
|
tls_t *tls = tls_new(conn);
|
|
|
|
/* If we couldn't init tls, it isn't there, so go on */
|
|
if (!tls) {
|
|
conn->tls_support = 0;
|
|
_auth(conn);
|
|
return;
|
|
} else {
|
|
tls_free(tls);
|
|
}
|
|
|
|
auth = _make_starttls(conn);
|
|
|
|
if (!auth) {
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
|
|
handler_add(conn, _handle_proceedtls_default, XMPP_NS_TLS, NULL, NULL,
|
|
NULL);
|
|
|
|
send_stanza(conn, auth, XMPP_QUEUE_STROPHE);
|
|
|
|
/* TLS was tried, unset flag */
|
|
conn->tls_support = 0;
|
|
/* _auth() will be called later */
|
|
return;
|
|
}
|
|
|
|
if (conn->tls_mandatory && !xmpp_conn_is_secured(conn)) {
|
|
strophe_error(conn->ctx, "xmpp",
|
|
"TLS is not supported, but set as "
|
|
"mandatory for this connection");
|
|
conn_disconnect(conn);
|
|
return;
|
|
}
|
|
|
|
if (anonjid && (conn->sasl_support & SASL_MASK_ANONYMOUS)) {
|
|
/* some crap here */
|
|
auth = _make_sasl_auth(conn, "ANONYMOUS");
|
|
if (!auth) {
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
|
|
handler_add(conn, _handle_sasl_result, XMPP_NS_SASL, NULL, NULL,
|
|
"ANONYMOUS");
|
|
|
|
send_stanza(conn, auth, XMPP_QUEUE_STROPHE);
|
|
|
|
/* SASL ANONYMOUS was tried, unset flag */
|
|
conn->sasl_support &= ~SASL_MASK_ANONYMOUS;
|
|
} else if (conn->sasl_support & SASL_MASK_EXTERNAL) {
|
|
/* more crap here */
|
|
auth = _make_sasl_auth(conn, "EXTERNAL");
|
|
if (!auth) {
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
|
|
authdata = xmpp_stanza_new(conn->ctx);
|
|
if (!authdata) {
|
|
xmpp_stanza_release(auth);
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
str = tls_id_on_xmppaddr(conn, 0);
|
|
if (!str || (tls_id_on_xmppaddr_num(conn) == 1 &&
|
|
strcmp(str, conn->jid) == 0)) {
|
|
xmpp_stanza_set_text(authdata, "=");
|
|
} else {
|
|
strophe_free(conn->ctx, str);
|
|
str = xmpp_base64_encode(conn->ctx, (void *)conn->jid,
|
|
strlen(conn->jid));
|
|
if (!str) {
|
|
xmpp_stanza_release(authdata);
|
|
xmpp_stanza_release(auth);
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
xmpp_stanza_set_text(authdata, str);
|
|
}
|
|
strophe_free(conn->ctx, str);
|
|
|
|
xmpp_stanza_add_child_ex(auth, authdata, 0);
|
|
|
|
handler_add(conn, _handle_sasl_result, XMPP_NS_SASL, NULL, NULL,
|
|
"EXTERNAL");
|
|
|
|
send_stanza(conn, auth, XMPP_QUEUE_STROPHE);
|
|
|
|
/* SASL EXTERNAL was tried, unset flag */
|
|
conn->sasl_support &= ~SASL_MASK_EXTERNAL;
|
|
} else if (anonjid) {
|
|
strophe_error(conn->ctx, "auth",
|
|
"No node in JID, and SASL ANONYMOUS unsupported.");
|
|
xmpp_disconnect(conn);
|
|
} else if (conn->pass == NULL) {
|
|
strophe_error(
|
|
conn->ctx, "auth",
|
|
"Password hasn't been set, and SASL ANONYMOUS unsupported.");
|
|
xmpp_disconnect(conn);
|
|
} else if (conn->sasl_support & SASL_MASK_SCRAM) {
|
|
size_t n;
|
|
scram_ctx = strophe_alloc(conn->ctx, sizeof(*scram_ctx));
|
|
memset(scram_ctx, 0, sizeof(*scram_ctx));
|
|
for (n = 0; n < scram_algs_num; ++n) {
|
|
if (conn->sasl_support & scram_algs[n]->mask) {
|
|
scram_ctx->alg = scram_algs[n];
|
|
break;
|
|
}
|
|
}
|
|
|
|
auth = _make_sasl_auth(conn, scram_ctx->alg->scram_name);
|
|
if (!auth) {
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
|
|
scram_ctx->conn = conn;
|
|
scram_ctx->sasl_plus =
|
|
scram_ctx->alg->mask & SASL_MASK_SCRAM_PLUS ? 1 : 0;
|
|
if (_make_scram_init_msg(scram_ctx)) {
|
|
strophe_free(conn->ctx, scram_ctx);
|
|
xmpp_stanza_release(auth);
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
|
|
str = xmpp_base64_encode(conn->ctx,
|
|
(unsigned char *)scram_ctx->scram_init,
|
|
strlen(scram_ctx->scram_init));
|
|
if (!str) {
|
|
strophe_free(conn->ctx, scram_ctx->scram_init);
|
|
strophe_free(conn->ctx, scram_ctx);
|
|
xmpp_stanza_release(auth);
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
|
|
authdata = xmpp_stanza_new(conn->ctx);
|
|
if (!authdata) {
|
|
strophe_free(conn->ctx, str);
|
|
strophe_free(conn->ctx, scram_ctx->scram_init);
|
|
strophe_free(conn->ctx, scram_ctx);
|
|
xmpp_stanza_release(auth);
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
xmpp_stanza_set_text(authdata, str);
|
|
strophe_free(conn->ctx, str);
|
|
xmpp_stanza_add_child_ex(auth, authdata, 0);
|
|
|
|
handler_add(conn, _handle_scram_challenge, XMPP_NS_SASL, NULL, NULL,
|
|
(void *)scram_ctx);
|
|
|
|
send_stanza(conn, auth, XMPP_QUEUE_STROPHE);
|
|
|
|
/* SASL algorithm was tried, unset flag */
|
|
conn->sasl_support &= ~scram_ctx->alg->mask;
|
|
} else if (conn->sasl_support & SASL_MASK_DIGESTMD5) {
|
|
auth = _make_sasl_auth(conn, "DIGEST-MD5");
|
|
if (!auth) {
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
|
|
handler_add(conn, _handle_digestmd5_challenge, XMPP_NS_SASL, NULL, NULL,
|
|
NULL);
|
|
|
|
send_stanza(conn, auth, XMPP_QUEUE_STROPHE);
|
|
|
|
/* SASL DIGEST-MD5 was tried, unset flag */
|
|
conn->sasl_support &= ~SASL_MASK_DIGESTMD5;
|
|
} else if (conn->sasl_support & SASL_MASK_PLAIN) {
|
|
auth = _make_sasl_auth(conn, "PLAIN");
|
|
if (!auth) {
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
authdata = xmpp_stanza_new(conn->ctx);
|
|
if (!authdata) {
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
authid = _get_authid(conn);
|
|
if (!authid) {
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
str = sasl_plain(conn->ctx, authid, conn->pass);
|
|
if (!str) {
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
xmpp_stanza_set_text(authdata, str);
|
|
strophe_free(conn->ctx, str);
|
|
strophe_free(conn->ctx, authid);
|
|
|
|
xmpp_stanza_add_child_ex(auth, authdata, 0);
|
|
|
|
handler_add(conn, _handle_sasl_result, XMPP_NS_SASL, NULL, NULL,
|
|
"PLAIN");
|
|
|
|
send_stanza(conn, auth, XMPP_QUEUE_STROPHE);
|
|
|
|
/* SASL PLAIN was tried */
|
|
conn->sasl_support &= ~SASL_MASK_PLAIN;
|
|
} else if (conn->type == XMPP_CLIENT && conn->auth_legacy_enabled) {
|
|
/* legacy client authentication */
|
|
_auth_legacy(conn);
|
|
} else {
|
|
strophe_error(conn->ctx, "auth",
|
|
"Cannot authenticate with known methods");
|
|
xmpp_disconnect(conn);
|
|
}
|
|
}
|
|
|
|
static void _stream_negotiation_success(xmpp_conn_t *conn)
|
|
{
|
|
tls_clear_password_cache(conn);
|
|
conn->stream_negotiation_completed = 1;
|
|
/* call connection handler */
|
|
conn->conn_handler(conn, XMPP_CONN_CONNECT, 0, NULL, conn->userdata);
|
|
}
|
|
|
|
/** Set up handlers at stream start.
|
|
* This function is called internally to Strophe for handling the opening
|
|
* of an XMPP stream. It's called by the parser when a stream is opened
|
|
* or reset, and adds the initial handlers for <stream:error/> and
|
|
* <stream:features/>. This function is not intended for use outside
|
|
* of Strophe.
|
|
*
|
|
* @param conn a Strophe connection object
|
|
*/
|
|
void auth_handle_open(xmpp_conn_t *conn)
|
|
{
|
|
/* reset all timed handlers */
|
|
handler_reset_timed(conn, 0);
|
|
|
|
/* setup handler for stream:error, we will keep this handler
|
|
* for reopened streams until connection is disconnected */
|
|
handler_add(conn, _handle_error, XMPP_NS_STREAMS, "error", NULL, NULL);
|
|
|
|
/* setup handlers for incoming <stream:features> */
|
|
handler_add(conn, _handle_features, XMPP_NS_STREAMS, "features", NULL,
|
|
NULL);
|
|
handler_add_timed(conn, _handle_missing_features, FEATURES_TIMEOUT, NULL);
|
|
}
|
|
|
|
/* called when stream:stream tag received after TLS establishment */
|
|
static void _handle_open_tls(xmpp_conn_t *conn)
|
|
{
|
|
/* setup handlers for incoming <stream:features> */
|
|
handler_add(conn, _handle_features, XMPP_NS_STREAMS, "features", NULL,
|
|
NULL);
|
|
handler_add_timed(conn, _handle_missing_features, FEATURES_TIMEOUT, NULL);
|
|
}
|
|
|
|
/* called when stream:stream tag received after SASL auth */
|
|
static void _handle_open_sasl(xmpp_conn_t *conn)
|
|
{
|
|
strophe_debug(conn->ctx, "xmpp", "Reopened stream successfully.");
|
|
|
|
/* setup stream:features handlers */
|
|
handler_add(conn, _handle_features_sasl, XMPP_NS_STREAMS, "features", NULL,
|
|
NULL);
|
|
handler_add_timed(conn, _handle_missing_features_sasl, FEATURES_TIMEOUT,
|
|
NULL);
|
|
}
|
|
|
|
/* called when stream:stream tag received after compression has been enabled */
|
|
static void _handle_open_compress(xmpp_conn_t *conn)
|
|
{
|
|
strophe_debug(conn->ctx, "xmpp", "Reopened stream successfully.");
|
|
|
|
/* setup stream:features handlers */
|
|
handler_add(conn, _handle_features_compress, XMPP_NS_STREAMS, "features",
|
|
NULL, NULL);
|
|
handler_add_timed(conn, _handle_missing_features, FEATURES_TIMEOUT, NULL);
|
|
}
|
|
|
|
static int _do_bind(xmpp_conn_t *conn, xmpp_stanza_t *bind)
|
|
{
|
|
xmpp_stanza_t *iq, *res, *text;
|
|
char *resource;
|
|
|
|
/* setup response handlers */
|
|
handler_add_id(conn, _handle_bind, "_xmpp_bind1", NULL);
|
|
handler_add_timed(conn, _handle_missing_bind, BIND_TIMEOUT, NULL);
|
|
|
|
/* send bind request */
|
|
iq = xmpp_iq_new(conn->ctx, "set", "_xmpp_bind1");
|
|
if (!iq) {
|
|
xmpp_stanza_release(bind);
|
|
disconnect_mem_error(conn);
|
|
return 0;
|
|
}
|
|
|
|
/* request a specific resource if we have one */
|
|
resource = xmpp_jid_resource(conn->ctx, conn->jid);
|
|
if ((resource != NULL) && (strlen(resource) == 0)) {
|
|
/* jabberd2 doesn't handle an empty resource */
|
|
strophe_free(conn->ctx, resource);
|
|
resource = NULL;
|
|
}
|
|
|
|
/* if we have a resource to request, do it. otherwise the
|
|
server will assign us one */
|
|
if (resource) {
|
|
res = xmpp_stanza_new(conn->ctx);
|
|
if (!res) {
|
|
xmpp_stanza_release(bind);
|
|
xmpp_stanza_release(iq);
|
|
disconnect_mem_error(conn);
|
|
return 0;
|
|
}
|
|
xmpp_stanza_set_name(res, "resource");
|
|
text = xmpp_stanza_new(conn->ctx);
|
|
if (!text) {
|
|
xmpp_stanza_release(res);
|
|
xmpp_stanza_release(bind);
|
|
xmpp_stanza_release(iq);
|
|
disconnect_mem_error(conn);
|
|
return 0;
|
|
}
|
|
xmpp_stanza_set_text(text, resource);
|
|
xmpp_stanza_add_child_ex(res, text, 0);
|
|
xmpp_stanza_add_child_ex(bind, res, 0);
|
|
strophe_free(conn->ctx, resource);
|
|
}
|
|
|
|
xmpp_stanza_add_child_ex(iq, bind, 0);
|
|
|
|
/* send bind request */
|
|
send_stanza(conn, iq, XMPP_QUEUE_STROPHE);
|
|
return 0;
|
|
}
|
|
|
|
static int _handle_compress_result(xmpp_conn_t *const conn,
|
|
xmpp_stanza_t *const stanza,
|
|
void *const userdata)
|
|
{
|
|
const char *name = xmpp_stanza_get_name(stanza);
|
|
|
|
UNUSED(userdata);
|
|
|
|
if (!name)
|
|
return 0;
|
|
if (strcmp(name, "compressed") == 0) {
|
|
/* Stream compression enabled, we need to restart the stream */
|
|
strophe_debug(conn->ctx, "xmpp", "Stream compression enabled");
|
|
|
|
/* reset parser */
|
|
conn_prepare_reset(conn, _handle_open_sasl);
|
|
|
|
/* make compression effective */
|
|
compression_init(conn);
|
|
|
|
/* send stream tag */
|
|
conn_open_stream(conn);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int _handle_features_compress(xmpp_conn_t *conn,
|
|
xmpp_stanza_t *stanza,
|
|
void *userdata)
|
|
{
|
|
const char *compress = "<compress xmlns='" XMPP_NS_COMPRESSION
|
|
"'><method>zlib</method></compress>";
|
|
xmpp_stanza_t *child;
|
|
|
|
/* remove missing features handler */
|
|
xmpp_timed_handler_delete(conn, _handle_missing_features);
|
|
|
|
/* check for compression */
|
|
child = xmpp_stanza_get_child_by_name_and_ns(stanza, "compression",
|
|
XMPP_NS_FEATURE_COMPRESSION);
|
|
if (conn->compression.allowed && child) {
|
|
_foreach_child(conn, child, "method",
|
|
compression_handle_feature_children);
|
|
}
|
|
|
|
if (conn->compression.supported) {
|
|
send_raw(conn, compress, strlen(compress), XMPP_QUEUE_STROPHE, NULL);
|
|
handler_add(conn, _handle_compress_result, XMPP_NS_COMPRESSION, NULL,
|
|
NULL, NULL);
|
|
} else {
|
|
return _handle_features_sasl(conn, stanza, userdata);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
_handle_features_sasl(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata)
|
|
{
|
|
xmpp_stanza_t *bind, *session;
|
|
xmpp_stanza_t *resume;
|
|
char h[11];
|
|
|
|
UNUSED(userdata);
|
|
|
|
/* Remove missing features handler */
|
|
xmpp_timed_handler_delete(conn, _handle_missing_features_sasl);
|
|
|
|
/* Check whether resource binding is required */
|
|
bind = xmpp_stanza_get_child_by_name_and_ns(stanza, "bind", XMPP_NS_BIND);
|
|
if (bind) {
|
|
conn->bind_required = 1;
|
|
bind = xmpp_stanza_copy(bind);
|
|
if (!bind) {
|
|
disconnect_mem_error(conn);
|
|
return 0;
|
|
}
|
|
} else {
|
|
conn->bind_required = 0;
|
|
}
|
|
|
|
/* Check whether session establishment is required.
|
|
*
|
|
* The mechanism is deprecated, but we still support it.
|
|
*
|
|
* RFC3921 contains Ch. 3 "Session Establishment".
|
|
*
|
|
* RFC6121 removes this and explains in Ch. 1.4:
|
|
* "Interoperability Note: [...] Implementation and deployment experience
|
|
* has shown that this additional step is unnecessary. [...]" */
|
|
session = xmpp_stanza_get_child_by_name_and_ns(stanza, "session",
|
|
XMPP_NS_SESSION);
|
|
if (session) {
|
|
conn->session_required =
|
|
xmpp_stanza_get_child_by_name(session, "optional") == NULL;
|
|
}
|
|
|
|
/* Check stream-management support */
|
|
if (xmpp_stanza_get_child_by_name_and_ns(stanza, "sm", XMPP_NS_SM)) {
|
|
conn->sm_state->sm_support = 1;
|
|
}
|
|
|
|
/* We are expecting either <bind/> and optionally <session/> since this is a
|
|
XMPP style connection or we <resume/> the previous session */
|
|
|
|
/* Check whether we can <resume/> the previous session */
|
|
if (!conn->sm_disable && conn->sm_state->can_resume &&
|
|
conn->sm_state->previd && conn->sm_state->bound_jid) {
|
|
resume = xmpp_stanza_new(conn->ctx);
|
|
if (!resume) {
|
|
disconnect_mem_error(conn);
|
|
return 0;
|
|
}
|
|
conn->sm_state->bind = bind;
|
|
conn->sm_state->resume = 1;
|
|
xmpp_stanza_set_name(resume, "resume");
|
|
xmpp_stanza_set_ns(resume, XMPP_NS_SM);
|
|
xmpp_stanza_set_attribute(resume, "previd", conn->sm_state->previd);
|
|
strophe_snprintf(h, sizeof(h), "%u", conn->sm_state->sm_handled_nr);
|
|
xmpp_stanza_set_attribute(resume, "h", h);
|
|
send_stanza(conn, resume, XMPP_QUEUE_SM_STROPHE);
|
|
handler_add(conn, _handle_sm, XMPP_NS_SM, NULL, NULL, NULL);
|
|
}
|
|
/* if bind is required, go ahead and start it */
|
|
else if (conn->bind_required) {
|
|
/* bind resource */
|
|
_do_bind(conn, bind);
|
|
} else {
|
|
/* can't bind, disconnect */
|
|
if (bind) {
|
|
xmpp_stanza_release(bind);
|
|
}
|
|
strophe_error(conn->ctx, "xmpp",
|
|
"Stream features does not allow "
|
|
"resource bind.");
|
|
xmpp_disconnect(conn);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int _handle_missing_features_sasl(xmpp_conn_t *conn, void *userdata)
|
|
{
|
|
UNUSED(userdata);
|
|
|
|
strophe_error(conn->ctx, "xmpp",
|
|
"Did not receive stream features "
|
|
"after SASL authentication.");
|
|
xmpp_disconnect(conn);
|
|
return 0;
|
|
}
|
|
|
|
static void _session_start(xmpp_conn_t *conn)
|
|
{
|
|
xmpp_stanza_t *session;
|
|
xmpp_stanza_t *iq = xmpp_iq_new(conn->ctx, "set", "_xmpp_session1");
|
|
if (!iq) {
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
|
|
session = xmpp_stanza_new(conn->ctx);
|
|
if (!session) {
|
|
xmpp_stanza_release(iq);
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
|
|
/* setup response handlers */
|
|
handler_add_id(conn, _handle_session, "_xmpp_session1", NULL);
|
|
handler_add_timed(conn, _handle_missing_session, SESSION_TIMEOUT, NULL);
|
|
|
|
xmpp_stanza_set_name(session, "session");
|
|
xmpp_stanza_set_ns(session, XMPP_NS_SESSION);
|
|
|
|
xmpp_stanza_add_child_ex(iq, session, 0);
|
|
|
|
/* send session establishment request */
|
|
send_stanza(conn, iq, XMPP_QUEUE_STROPHE);
|
|
}
|
|
|
|
static void _sm_enable(xmpp_conn_t *conn)
|
|
{
|
|
xmpp_stanza_t *enable = xmpp_stanza_new(conn->ctx);
|
|
if (!enable) {
|
|
disconnect_mem_error(conn);
|
|
return;
|
|
}
|
|
xmpp_stanza_set_name(enable, "enable");
|
|
xmpp_stanza_set_ns(enable, XMPP_NS_SM);
|
|
if (!conn->sm_state->dont_request_resume)
|
|
xmpp_stanza_set_attribute(enable, "resume", "true");
|
|
handler_add(conn, _handle_sm, XMPP_NS_SM, NULL, NULL, NULL);
|
|
send_stanza(conn, enable, XMPP_QUEUE_SM_STROPHE);
|
|
conn->sm_state->sm_sent_nr = 0;
|
|
conn->sm_state->sm_enabled = 1;
|
|
trigger_sm_callback(conn);
|
|
}
|
|
|
|
static int
|
|
_handle_bind(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata)
|
|
{
|
|
const char *type;
|
|
xmpp_stanza_t *binding, *jid_stanza;
|
|
|
|
UNUSED(userdata);
|
|
|
|
/* delete missing bind handler */
|
|
xmpp_timed_handler_delete(conn, _handle_missing_bind);
|
|
|
|
/* server has replied to bind request */
|
|
type = xmpp_stanza_get_type(stanza);
|
|
if (type && strcmp(type, "error") == 0) {
|
|
strophe_error(conn->ctx, "xmpp", "Binding failed.");
|
|
xmpp_disconnect(conn);
|
|
} else if (type && strcmp(type, "result") == 0) {
|
|
binding = xmpp_stanza_get_child_by_name(stanza, "bind");
|
|
strophe_debug(conn->ctx, "xmpp", "Bind successful.");
|
|
|
|
if (binding) {
|
|
jid_stanza = xmpp_stanza_get_child_by_name(binding, "jid");
|
|
if (jid_stanza) {
|
|
conn->bound_jid = xmpp_stanza_get_text(jid_stanza);
|
|
}
|
|
}
|
|
|
|
/* establish a session if required */
|
|
if (conn->session_required) {
|
|
_session_start(conn);
|
|
}
|
|
/* send enable directly after the bind request */
|
|
else if (conn->sm_state->sm_support && !conn->sm_disable) {
|
|
_sm_enable(conn);
|
|
}
|
|
/* if there's no xmpp session required and we didn't try to enable
|
|
* stream-management, we're done here and the stream-negotiation was
|
|
* successful
|
|
*/
|
|
else {
|
|
_stream_negotiation_success(conn);
|
|
}
|
|
} else {
|
|
strophe_error(conn->ctx, "xmpp", "Server sent malformed bind reply.");
|
|
xmpp_disconnect(conn);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int _handle_missing_bind(xmpp_conn_t *conn, void *userdata)
|
|
{
|
|
UNUSED(userdata);
|
|
|
|
strophe_error(conn->ctx, "xmpp", "Server did not reply to bind request.");
|
|
xmpp_disconnect(conn);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
_handle_session(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata)
|
|
{
|
|
const char *type;
|
|
|
|
UNUSED(userdata);
|
|
|
|
/* delete missing session handler */
|
|
xmpp_timed_handler_delete(conn, _handle_missing_session);
|
|
|
|
/* server has replied to the session request */
|
|
type = xmpp_stanza_get_type(stanza);
|
|
if (type && strcmp(type, "error") == 0) {
|
|
strophe_error(conn->ctx, "xmpp", "Session establishment failed.");
|
|
xmpp_disconnect(conn);
|
|
} else if (type && strcmp(type, "result") == 0) {
|
|
strophe_debug(conn->ctx, "xmpp", "Session establishment successful.");
|
|
if (conn->sm_state->sm_support && !conn->sm_disable) {
|
|
_sm_enable(conn);
|
|
} else {
|
|
_stream_negotiation_success(conn);
|
|
}
|
|
} else {
|
|
strophe_error(conn->ctx, "xmpp",
|
|
"Server sent malformed session reply.");
|
|
xmpp_disconnect(conn);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int _handle_missing_session(xmpp_conn_t *conn, void *userdata)
|
|
{
|
|
UNUSED(userdata);
|
|
|
|
strophe_error(conn->ctx, "xmpp",
|
|
"Server did not reply to session request.");
|
|
xmpp_disconnect(conn);
|
|
return 0;
|
|
}
|
|
|
|
static int _handle_missing_legacy(xmpp_conn_t *conn, void *userdata)
|
|
{
|
|
UNUSED(userdata);
|
|
|
|
strophe_error(conn->ctx, "xmpp",
|
|
"Server did not reply to legacy "
|
|
"authentication request.");
|
|
xmpp_disconnect(conn);
|
|
return 0;
|
|
}
|
|
|
|
static int _get_h_attribute(xmpp_stanza_t *stanza, unsigned long *ul_h)
|
|
{
|
|
const char *h = xmpp_stanza_get_attribute(stanza, "h");
|
|
if (!h || string_to_ul(h, ul_h)) {
|
|
strophe_error(
|
|
stanza->ctx, "xmpp",
|
|
"SM error: failed parsing 'h', \"%s\" got converted to %llu.",
|
|
STR_MAYBE_NULL(h), *ul_h);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void _sm_queue_cleanup(xmpp_conn_t *conn, unsigned long ul_h)
|
|
{
|
|
xmpp_send_queue_t *e;
|
|
while ((e = peek_queue_front(&conn->sm_state->sm_queue))) {
|
|
if (e->sm_h >= ul_h)
|
|
break;
|
|
e = pop_queue_front(&conn->sm_state->sm_queue);
|
|
strophe_free(conn->ctx, queue_element_free(conn->ctx, e));
|
|
}
|
|
}
|
|
|
|
static void _sm_queue_resend(xmpp_conn_t *conn)
|
|
{
|
|
xmpp_send_queue_t *e;
|
|
while ((e = pop_queue_front(&conn->sm_state->sm_queue))) {
|
|
/* Re-send what was already sent out and is still in the
|
|
* SM queue (i.e. it hasn't been ACK'ed by the server)
|
|
*/
|
|
strophe_debug_verbose(2, conn->ctx, "conn", "SM_Q_RESEND: %p, h=%lu", e,
|
|
e->sm_h);
|
|
send_raw(conn, e->data, e->len, e->owner, NULL);
|
|
strophe_free(conn->ctx, queue_element_free(conn->ctx, e));
|
|
}
|
|
}
|
|
|
|
static int _handle_sm(xmpp_conn_t *const conn,
|
|
xmpp_stanza_t *const stanza,
|
|
void *const userdata)
|
|
{
|
|
xmpp_stanza_t *failed_cause, *bind = NULL;
|
|
const char *name, *id, *previd, *resume, *cause;
|
|
unsigned long ul_h = 0;
|
|
|
|
UNUSED(userdata);
|
|
|
|
name = xmpp_stanza_get_name(stanza);
|
|
if (!name)
|
|
goto err_sm;
|
|
|
|
if (strcmp(name, "enabled") == 0) {
|
|
conn->sm_state->sm_handled_nr = 0;
|
|
resume = xmpp_stanza_get_attribute(stanza, "resume");
|
|
if (resume && (strcasecmp(resume, "true") || strcmp(resume, "1"))) {
|
|
id = xmpp_stanza_get_attribute(stanza, "id");
|
|
if (!id) {
|
|
strophe_error(conn->ctx, "xmpp",
|
|
"SM error: server said it can resume, but "
|
|
"didn't provide an ID.");
|
|
name = NULL;
|
|
goto err_sm;
|
|
}
|
|
conn->sm_state->can_resume = 1;
|
|
conn->sm_state->id = strophe_strdup(conn->ctx, id);
|
|
}
|
|
/* We maybe have stuff in the SM queue if we tried to resume, but the
|
|
* server doesn't remember all details of our session, but the `h` was
|
|
* still available.
|
|
*/
|
|
_sm_queue_resend(conn);
|
|
_stream_negotiation_success(conn);
|
|
} else if (strcmp(name, "resumed") == 0) {
|
|
previd = xmpp_stanza_get_attribute(stanza, "previd");
|
|
if (!previd || strcmp(previd, conn->sm_state->previd)) {
|
|
strophe_error(conn->ctx, "xmpp",
|
|
"SM error: previd didn't match, ours is \"%s\".",
|
|
conn->sm_state->previd);
|
|
name = NULL;
|
|
goto err_sm;
|
|
}
|
|
if (_get_h_attribute(stanza, &ul_h)) {
|
|
name = NULL;
|
|
goto err_sm;
|
|
}
|
|
conn->sm_state->sm_enabled = 1;
|
|
conn->sm_state->id = conn->sm_state->previd;
|
|
conn->sm_state->previd = NULL;
|
|
conn->bound_jid = conn->sm_state->bound_jid;
|
|
conn->sm_state->bound_jid = NULL;
|
|
if (conn->sm_state->sm_queue.head)
|
|
conn->sm_state->sm_sent_nr = conn->sm_state->sm_queue.head->sm_h;
|
|
else
|
|
conn->sm_state->sm_sent_nr = ul_h;
|
|
_sm_queue_cleanup(conn, ul_h);
|
|
_sm_queue_resend(conn);
|
|
strophe_debug(conn->ctx, "xmpp", "Session resumed successfully.");
|
|
_stream_negotiation_success(conn);
|
|
} else if (strcmp(name, "failed") == 0) {
|
|
name = NULL;
|
|
conn->sm_state->sm_enabled = 0;
|
|
|
|
failed_cause =
|
|
xmpp_stanza_get_child_by_ns(stanza, XMPP_NS_STANZAS_IETF);
|
|
if (!failed_cause)
|
|
goto err_sm;
|
|
|
|
cause = xmpp_stanza_get_name(failed_cause);
|
|
if (!cause)
|
|
goto err_sm;
|
|
|
|
if (!strcmp(cause, "item-not-found")) {
|
|
if (conn->sm_state->resume) {
|
|
/* It's no error if there's no `h` attribute included
|
|
* but if there is, it gives a hint at what the server
|
|
* already received.
|
|
*/
|
|
if (!_get_h_attribute(stanza, &ul_h)) {
|
|
/* In cases there's no `h` included, drop all elements. */
|
|
ul_h = (unsigned long)-1;
|
|
}
|
|
_sm_queue_cleanup(conn, ul_h);
|
|
}
|
|
} else if (!strcmp(cause, "feature-not-implemented")) {
|
|
conn->sm_state->resume = 0;
|
|
conn->sm_state->can_resume = 0;
|
|
/* remember that the server reports having support
|
|
* for resumption, but actually it doesn't ...
|
|
*/
|
|
conn->sm_state->dont_request_resume = 1;
|
|
}
|
|
bind = conn->sm_state->bind;
|
|
conn->sm_state->bind = NULL;
|
|
reset_sm_state(conn->sm_state);
|
|
_do_bind(conn, bind);
|
|
} else {
|
|
/* unknown stanza received */
|
|
name = NULL;
|
|
}
|
|
|
|
trigger_sm_callback(conn);
|
|
|
|
err_sm:
|
|
if (!name) {
|
|
char *err = "Couldn't convert stanza to text!";
|
|
char *buf;
|
|
size_t buflen;
|
|
switch (xmpp_stanza_to_text(stanza, &buf, &buflen)) {
|
|
case XMPP_EOK:
|
|
break;
|
|
case XMPP_EMEM:
|
|
disconnect_mem_error(conn);
|
|
return 0;
|
|
default:
|
|
buf = err;
|
|
break;
|
|
}
|
|
strophe_warn(conn->ctx, "xmpp", "SM error: Stanza received was: %s",
|
|
buf);
|
|
if (buf != err)
|
|
strophe_free(conn->ctx, buf);
|
|
/* Don't disable for <failure> cases, they're no hard errors */
|
|
conn->sm_state->sm_enabled = bind != NULL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
_handle_legacy(xmpp_conn_t *conn, xmpp_stanza_t *stanza, void *userdata)
|
|
{
|
|
const char *type;
|
|
const char *name;
|
|
|
|
UNUSED(userdata);
|
|
|
|
/* delete missing handler */
|
|
xmpp_timed_handler_delete(conn, _handle_missing_legacy);
|
|
|
|
/* server responded to legacy auth request */
|
|
type = xmpp_stanza_get_type(stanza);
|
|
name = xmpp_stanza_get_name(stanza);
|
|
if (!type || strcmp(name, "iq") != 0) {
|
|
strophe_error(conn->ctx, "xmpp",
|
|
"Server sent us an unexpected response "
|
|
"to legacy authentication request.");
|
|
xmpp_disconnect(conn);
|
|
} else if (strcmp(type, "error") == 0) {
|
|
/* legacy client auth failed, no more fallbacks */
|
|
strophe_error(conn->ctx, "xmpp",
|
|
"Legacy client authentication failed.");
|
|
xmpp_disconnect(conn);
|
|
} else if (strcmp(type, "result") == 0) {
|
|
/* auth succeeded */
|
|
strophe_debug(conn->ctx, "xmpp", "Legacy auth succeeded.");
|
|
|
|
_stream_negotiation_success(conn);
|
|
} else {
|
|
strophe_error(conn->ctx, "xmpp",
|
|
"Server sent us a legacy authentication "
|
|
"response with a bad type.");
|
|
xmpp_disconnect(conn);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void _auth_legacy(xmpp_conn_t *conn)
|
|
{
|
|
xmpp_stanza_t *iq;
|
|
xmpp_stanza_t *authdata;
|
|
xmpp_stanza_t *query;
|
|
xmpp_stanza_t *child;
|
|
char *str;
|
|
|
|
strophe_debug(conn->ctx, "auth", "Legacy authentication request");
|
|
|
|
iq = xmpp_iq_new(conn->ctx, "set", "_xmpp_auth1");
|
|
if (!iq)
|
|
goto err;
|
|
|
|
query = xmpp_stanza_new(conn->ctx);
|
|
if (!query)
|
|
goto err_free;
|
|
xmpp_stanza_set_name(query, "query");
|
|
xmpp_stanza_set_ns(query, XMPP_NS_AUTH);
|
|
xmpp_stanza_add_child_ex(iq, query, 0);
|
|
|
|
child = xmpp_stanza_new(conn->ctx);
|
|
if (!child)
|
|
goto err_free;
|
|
xmpp_stanza_set_name(child, "username");
|
|
xmpp_stanza_add_child_ex(query, child, 0);
|
|
|
|
authdata = xmpp_stanza_new(conn->ctx);
|
|
if (!authdata)
|
|
goto err_free;
|
|
str = xmpp_jid_node(conn->ctx, conn->jid);
|
|
if (!str) {
|
|
xmpp_stanza_release(authdata);
|
|
goto err_free;
|
|
}
|
|
xmpp_stanza_set_text(authdata, str);
|
|
strophe_free(conn->ctx, str);
|
|
xmpp_stanza_add_child_ex(child, authdata, 0);
|
|
|
|
child = xmpp_stanza_new(conn->ctx);
|
|
if (!child)
|
|
goto err_free;
|
|
xmpp_stanza_set_name(child, "password");
|
|
xmpp_stanza_add_child_ex(query, child, 0);
|
|
|
|
authdata = xmpp_stanza_new(conn->ctx);
|
|
if (!authdata)
|
|
goto err_free;
|
|
xmpp_stanza_set_text(authdata, conn->pass);
|
|
xmpp_stanza_add_child_ex(child, authdata, 0);
|
|
|
|
child = xmpp_stanza_new(conn->ctx);
|
|
if (!child)
|
|
goto err_free;
|
|
xmpp_stanza_set_name(child, "resource");
|
|
xmpp_stanza_add_child_ex(query, child, 0);
|
|
|
|
authdata = xmpp_stanza_new(conn->ctx);
|
|
if (!authdata)
|
|
goto err_free;
|
|
str = xmpp_jid_resource(conn->ctx, conn->jid);
|
|
if (str) {
|
|
xmpp_stanza_set_text(authdata, str);
|
|
strophe_free(conn->ctx, str);
|
|
} else {
|
|
xmpp_stanza_release(authdata);
|
|
xmpp_stanza_release(iq);
|
|
strophe_error(conn->ctx, "auth",
|
|
"Cannot authenticate without resource");
|
|
xmpp_disconnect(conn);
|
|
return;
|
|
}
|
|
xmpp_stanza_add_child_ex(child, authdata, 0);
|
|
|
|
handler_add_id(conn, _handle_legacy, "_xmpp_auth1", NULL);
|
|
handler_add_timed(conn, _handle_missing_legacy, LEGACY_TIMEOUT, NULL);
|
|
|
|
send_stanza(conn, iq, XMPP_QUEUE_STROPHE);
|
|
return;
|
|
|
|
err_free:
|
|
xmpp_stanza_release(iq);
|
|
err:
|
|
disconnect_mem_error(conn);
|
|
}
|
|
|
|
void auth_handle_component_open(xmpp_conn_t *conn)
|
|
{
|
|
int rc;
|
|
|
|
/* reset all timed handlers */
|
|
handler_reset_timed(conn, 0);
|
|
|
|
handler_add(conn, _handle_error, XMPP_NS_STREAMS, "error", NULL, NULL);
|
|
handler_add(conn, _handle_component_hs_response, NULL, "handshake", NULL,
|
|
NULL);
|
|
handler_add_timed(conn, _handle_missing_handshake, HANDSHAKE_TIMEOUT, NULL);
|
|
|
|
rc = _handle_component_auth(conn);
|
|
if (rc != 0) {
|
|
strophe_error(conn->ctx, "auth", "Component authentication failed.");
|
|
xmpp_disconnect(conn);
|
|
}
|
|
}
|
|
|
|
/* Will compute SHA1 and authenticate the component to the server */
|
|
int _handle_component_auth(xmpp_conn_t *conn)
|
|
{
|
|
uint8_t md_value[SHA1_DIGEST_SIZE];
|
|
SHA1_CTX mdctx;
|
|
char *digest;
|
|
size_t i;
|
|
|
|
if (conn->stream_id == NULL) {
|
|
strophe_error(conn->ctx, "auth",
|
|
"Received no stream id from the server.");
|
|
return XMPP_EINT;
|
|
}
|
|
|
|
/* Feed the session id and passphrase to the algorithm.
|
|
* We need to compute SHA1(session_id + passphrase)
|
|
*/
|
|
crypto_SHA1_Init(&mdctx);
|
|
crypto_SHA1_Update(&mdctx, (uint8_t *)conn->stream_id,
|
|
strlen(conn->stream_id));
|
|
crypto_SHA1_Update(&mdctx, (uint8_t *)conn->pass, strlen(conn->pass));
|
|
crypto_SHA1_Final(&mdctx, md_value);
|
|
|
|
digest = strophe_alloc(conn->ctx, 2 * sizeof(md_value) + 1);
|
|
if (digest) {
|
|
/* convert the digest into string representation */
|
|
for (i = 0; i < sizeof(md_value); i++)
|
|
strophe_snprintf(digest + i * 2, 3, "%02x", md_value[i]);
|
|
digest[2 * sizeof(md_value)] = '\0';
|
|
|
|
strophe_debug(conn->ctx, "auth", "Digest: %s, len: %d", digest,
|
|
strlen(digest));
|
|
|
|
/* Send the digest to the server */
|
|
send_raw_string(conn, "<handshake xmlns='%s'>%s</handshake>",
|
|
XMPP_NS_COMPONENT, digest);
|
|
strophe_debug(conn->ctx, "auth",
|
|
"Sent component handshake to the server.");
|
|
strophe_free(conn->ctx, digest);
|
|
} else {
|
|
strophe_debug(conn->ctx, "auth",
|
|
"Couldn't allocate memory for component "
|
|
"handshake digest.");
|
|
return XMPP_EMEM;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Check if the received stanza is <handshake/> and set auth to true
|
|
* and fire connection handler.
|
|
*/
|
|
int _handle_component_hs_response(xmpp_conn_t *conn,
|
|
xmpp_stanza_t *stanza,
|
|
void *userdata)
|
|
{
|
|
const char *name;
|
|
|
|
UNUSED(userdata);
|
|
|
|
xmpp_timed_handler_delete(conn, _handle_missing_handshake);
|
|
|
|
name = xmpp_stanza_get_name(stanza);
|
|
if (strcmp(name, "handshake") != 0) {
|
|
char *msg;
|
|
size_t msg_size;
|
|
xmpp_stanza_to_text(stanza, &msg, &msg_size);
|
|
if (msg) {
|
|
strophe_debug(conn->ctx, "auth", "Handshake failed: %s", msg);
|
|
strophe_free(conn->ctx, msg);
|
|
}
|
|
xmpp_disconnect(conn);
|
|
return XMPP_EINT;
|
|
} else {
|
|
_stream_negotiation_success(conn);
|
|
}
|
|
|
|
/* We don't need this handler anymore, return 0 so it can be deleted
|
|
* from the list of handlers.
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
int _handle_missing_handshake(xmpp_conn_t *conn, void *userdata)
|
|
{
|
|
UNUSED(userdata);
|
|
|
|
strophe_error(conn->ctx, "xmpp",
|
|
"Server did not reply to handshake request.");
|
|
xmpp_disconnect(conn);
|
|
return 0;
|
|
}
|
|
|
|
void auth_handle_open_raw(xmpp_conn_t *conn)
|
|
{
|
|
handler_reset_timed(conn, 0);
|
|
/* user handlers are not called before stream negotiation has completed. */
|
|
_stream_negotiation_success(conn);
|
|
}
|
|
|
|
void auth_handle_open_stub(xmpp_conn_t *conn)
|
|
{
|
|
strophe_warn(conn->ctx, "auth", "Stub callback is called.");
|
|
}
|