1
0
mirror of https://github.com/weechat/weechat.git synced 2026-06-12 14:14:48 +02:00
Files
weechat/src/plugins/relay/api/remote/relay-remote-network.c
T
2025-11-22 14:52:02 +01:00

1506 lines
48 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* SPDX-FileCopyrightText: 2024-2025 Sébastien Helleu <flashcode@flashtux.org>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* This file is part of WeeChat, the extensible chat client.
*
* WeeChat is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* WeeChat is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with WeeChat. If not, see <https://www.gnu.org/licenses/>.
*/
/* Network functions for relay remote */
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <gcrypt.h>
#include <gnutls/gnutls.h>
#include <gnutls/x509.h>
#include <cjson/cJSON.h>
#include "../../../weechat-plugin.h"
#include "../../relay.h"
#include "../../relay-auth.h"
#include "../../relay-config.h"
#include "../../relay-http.h"
#include "../../relay-raw.h"
#include "../../relay-remote.h"
#include "../../relay-websocket.h"
#include "../relay-api.h"
#include "relay-remote-event.h"
/*
* Gets URL to an API resource.
*
* For example if remote URL is "https://localhost:9000" and the resource is
* "handshake", it returns: "https://localhost:9000/api/handshake".
*
* Note: result must be freed after use.
*/
char *
relay_remote_network_get_url_resource (struct t_relay_remote *remote,
const char *resource)
{
char *url;
int colon_in_address;
if (!remote || !remote->address || !resource || !resource[0])
return NULL;
colon_in_address = (strchr (remote->address, ':')) ? 1 : 0;
weechat_asprintf (&url, "%s://%s%s%s:%d/api/%s",
(remote->tls) ? "https" : "http",
(colon_in_address) ? "[" : "",
remote->address,
(colon_in_address) ? "]" : "",
remote->port,
resource);
return url;
}
/*
* Close connection with remote.
*/
void
relay_remote_network_close_connection (struct t_relay_remote *remote)
{
if (!remote)
return;
if (remote->hook_url_handshake)
{
weechat_unhook (remote->hook_url_handshake);
remote->hook_url_handshake = NULL;
}
if (remote->hook_connect)
{
weechat_unhook (remote->hook_connect);
remote->hook_connect = NULL;
}
if (remote->hook_fd)
{
weechat_unhook (remote->hook_fd);
remote->hook_fd = NULL;
}
if (remote->sock != -1)
{
#ifdef _WIN32
closesocket (remote->sock);
#else
close (remote->sock);
#endif /* _WIN32 */
remote->sock = -1;
}
relay_websocket_deflate_reinit (remote->ws_deflate);
remote->version_ok = 0;
remote->synced = 0;
if (remote->partial_ws_frame)
{
free (remote->partial_ws_frame);
remote->partial_ws_frame = NULL;
}
remote->partial_ws_frame_size = 0;
}
/*
* Disconnects from remote.
*/
void
relay_remote_network_disconnect (struct t_relay_remote *remote)
{
if (!remote)
return;
relay_remote_network_close_connection (remote);
relay_remote_set_status (remote, RELAY_STATUS_DISCONNECTED);
weechat_printf (NULL, _("remote[%s]: disconnected"), remote->name);
relay_remote_reconnect_schedule (remote);
}
/*
* Checks if authentication via websocket handshake was successful.
*
* Returns:
* 1: authentication successful
* 0: authentication has failed
*/
int
relay_remote_network_check_auth (struct t_relay_remote *remote,
const char *buffer)
{
struct t_relay_http_response *http_resp;
cJSON *json_body, *json_error;
const char *msg_error, *msg_resp_error, *ptr_ws_accept;
char *key, hash[160 / 8], sec_websocket_accept[128];
int accept_ok, hash_size;
http_resp = NULL;
msg_error = NULL;
msg_resp_error = NULL;
accept_ok = 0;
http_resp = relay_http_parse_response (buffer);
if (!http_resp)
{
msg_error = _("invalid response from remote relay");
goto error;
}
if (http_resp->body)
{
json_body = cJSON_Parse (http_resp->body);
if (json_body)
{
json_error = cJSON_GetObjectItem (json_body, "error");
if (json_error && cJSON_IsString (json_error))
msg_resp_error = cJSON_GetStringValue (json_error);
}
}
if ((http_resp->return_code != 101)
|| (weechat_strcasecmp (http_resp->message, "Switching Protocols") != 0))
{
if (http_resp->return_code == 401)
msg_error = _("authentication failed with remote relay");
else
msg_error = _("invalid response from remote relay");
goto error;
}
if (remote->websocket_key)
{
ptr_ws_accept = weechat_hashtable_get (http_resp->headers,
"sec-websocket-accept");
if (ptr_ws_accept)
{
if (weechat_asprintf (&key, "%s%s", remote->websocket_key,
WEBSOCKET_GUID) >= 0)
{
if (weechat_crypto_hash (key, strlen (key), "sha1",
hash, &hash_size))
{
if (weechat_string_base_encode ("64", hash, hash_size,
sec_websocket_accept) > 0)
{
if (strcmp (ptr_ws_accept, sec_websocket_accept) == 0)
accept_ok = 1;
}
}
free (key);
}
}
}
relay_websocket_parse_extensions (
weechat_hashtable_get (http_resp->headers, "sec-websocket-extensions"),
remote->ws_deflate,
1); /* ws_deflate_allowed */
if (!accept_ok)
{
msg_error = _("invalid websocket response (handshake error)");
goto error;
}
relay_http_response_free (http_resp);
return 1;
error:
weechat_printf (
NULL,
_("%sremote[%s]: error: %s%s%s%s"),
weechat_prefix ("error"),
remote->name,
msg_error,
(msg_resp_error) ? " (" : "",
(msg_resp_error) ? msg_resp_error : "",
(msg_resp_error) ? ")" : "");
relay_http_response_free (http_resp);
return 0;
}
/*
* Sends data to the remote.
*
* Returns the number of bytes sent to the remote.
*/
int
relay_remote_network_send_data (struct t_relay_remote *remote,
const char *data, int data_size)
{
if (!remote)
return 0;
if (remote->tls)
{
return (remote->sock >= 0) ?
gnutls_record_send (remote->gnutls_sess, data, data_size) :
data_size;
}
else
{
return (remote->sock >= 0) ?
send (remote->sock, data, data_size, 0) :
data_size;
}
}
/*
* Sends data to the remote.
* If the remote is connected, encapsulate data in a websocket frame.
*
* Returns the number of bytes sent to the remote.
*/
int
relay_remote_network_send (struct t_relay_remote *remote,
enum t_relay_msg_type msg_type,
const char *data, int data_size)
{
const char *ptr_data;
char *websocket_frame;
unsigned long long length_frame;
int opcode, flags, num_sent;
if (!remote)
return 0;
ptr_data = data;
websocket_frame = NULL;
if (remote->status == RELAY_STATUS_CONNECTED)
{
/* encapsulate data in a websocket frame */
switch (msg_type)
{
case RELAY_MSG_PING:
opcode = WEBSOCKET_FRAME_OPCODE_PING;
break;
case RELAY_MSG_PONG:
opcode = WEBSOCKET_FRAME_OPCODE_PONG;
break;
case RELAY_MSG_CLOSE:
opcode = WEBSOCKET_FRAME_OPCODE_CLOSE;
break;
default:
opcode = WEBSOCKET_FRAME_OPCODE_TEXT;
break;
}
websocket_frame = relay_websocket_encode_frame (
remote->ws_deflate, opcode, 1, data, data_size, &length_frame);
if (websocket_frame)
{
ptr_data = websocket_frame;
data_size = length_frame;
}
}
num_sent = relay_remote_network_send_data (remote, ptr_data, data_size);
free (websocket_frame);
if (num_sent >= 0)
{
flags = RELAY_RAW_FLAG_SEND;
if ((msg_type == RELAY_MSG_PING)
|| (msg_type == RELAY_MSG_PONG)
|| (msg_type == RELAY_MSG_CLOSE))
{
flags |= RELAY_RAW_FLAG_BINARY;
}
relay_raw_print_remote (remote, msg_type, flags, data, data_size);
}
return num_sent;
}
/*
* Sends JSON data to the remote.
*
* Returns the number of bytes sent to the remote.
*/
int
relay_remote_network_send_json (struct t_relay_remote *remote, cJSON *json)
{
char *string;
int num_bytes;
if (!remote || !json)
return 0;
num_bytes = 0;
string = cJSON_PrintUnformatted (json);
if (string)
{
num_bytes = relay_remote_network_send (remote, RELAY_MSG_STANDARD,
string, strlen (string));
free (string);
}
return num_bytes;
}
/*
* Reads text buffer from a remote.
*/
void
relay_remote_network_recv_text (struct t_relay_remote *remote,
const char *buffer, int buffer_size)
{
char request[1024];
relay_raw_print_remote (remote, RELAY_MSG_STANDARD,
RELAY_RAW_FLAG_RECV,
buffer, buffer_size);
if (remote->status == RELAY_STATUS_AUTHENTICATING)
{
if (!relay_remote_network_check_auth (remote, buffer))
{
relay_remote_network_disconnect (remote);
return;
}
relay_remote_set_status (remote, RELAY_STATUS_CONNECTED);
remote->reconnect_delay = 0;
remote->reconnect_start = 0;
snprintf (request, sizeof (request),
"{\"request\": \"GET /api/version\"}");
relay_remote_network_send (remote, RELAY_MSG_STANDARD,
request, strlen (request));
}
else
{
relay_remote_event_recv (remote, buffer);
}
}
/*
* Reads websocket frames.
*/
void
relay_remote_network_read_websocket_frames (struct t_relay_remote *remote,
struct t_relay_websocket_frame *frames,
int num_frames)
{
int i;
if (!frames || (num_frames <= 0))
return;
for (i = 0; i < num_frames; i++)
{
if (frames[i].payload_size == 0)
{
/*
* When decoded length is 0, assume remote sent a PONG frame.
*
* RFC 6455 Section 5.5.3:
*
* "A Pong frame MAY be sent unsolicited. This serves as a
* unidirectional heartbeat. A response to an unsolicited
* Pong frame is not expected."
*/
continue;
}
switch (frames[i].opcode)
{
case RELAY_MSG_PING:
/* print message in raw buffer */
relay_raw_print_remote (remote, RELAY_MSG_PING,
RELAY_RAW_FLAG_RECV | RELAY_RAW_FLAG_BINARY,
frames[i].payload,
frames[i].payload_size);
/* answer with a PONG */
relay_remote_network_send (remote,
RELAY_MSG_PONG,
frames[i].payload,
frames[i].payload_size);
break;
case RELAY_MSG_CLOSE:
/* print message in raw buffer */
relay_raw_print_remote (remote, RELAY_MSG_CLOSE,
RELAY_RAW_FLAG_RECV | RELAY_RAW_FLAG_BINARY,
frames[i].payload,
frames[i].payload_size);
/* answer with a CLOSE */
relay_remote_network_send (remote,
RELAY_MSG_CLOSE,
frames[i].payload,
frames[i].payload_size);
/* close the connection */
relay_remote_network_disconnect (remote);
/* ignore any other message after the close */
return;
default:
if (frames[i].payload)
{
relay_remote_network_recv_text (remote,
frames[i].payload,
frames[i].payload_size);
}
break;
}
}
}
/*
* Reads a buffer of bytes from a remote.
*/
void
relay_remote_network_recv_buffer (struct t_relay_remote *remote,
const char *buffer, int buffer_size)
{
struct t_relay_websocket_frame *frames;
int rc, i, buffer2_size, num_frames;
char *buffer2;
/* if authenticating is in progress, check if it was successful */
if (remote->status == RELAY_STATUS_AUTHENTICATING)
{
relay_remote_network_recv_text (remote, buffer, buffer_size);
}
else if (remote->status == RELAY_STATUS_CONNECTED)
{
buffer2 = NULL;
buffer2_size = 0;
if (remote->partial_ws_frame)
{
buffer2_size = buffer_size + remote->partial_ws_frame_size;
buffer2 = malloc (buffer2_size);
if (!buffer2)
{
weechat_printf (
NULL,
_("%sremote[%s]: not enough memory"),
weechat_prefix ("error"),
remote->name);
return;
}
memcpy (buffer2, remote->partial_ws_frame,
remote->partial_ws_frame_size);
memcpy (buffer2 + remote->partial_ws_frame_size,
buffer, buffer_size);
}
frames = NULL;
num_frames = 0;
rc = relay_websocket_decode_frame (
(buffer2) ? (unsigned char *)buffer2 : (const unsigned char *)buffer,
(buffer2) ? (unsigned long long)buffer2_size : (unsigned long long)buffer_size,
0, /* expect_masked_frame */
remote->ws_deflate,
&frames,
&num_frames,
&remote->partial_ws_frame,
&remote->partial_ws_frame_size);
free (buffer2);
if (!rc)
{
/* fatal error when decoding frame: close connection */
if (frames)
{
for (i = 0; i < num_frames; i++)
{
free (frames[i].payload);
}
free (frames);
}
weechat_printf (
NULL,
_("%sremote[%s]: error decoding websocket frame"),
weechat_prefix ("error"),
remote->name);
relay_remote_network_disconnect (remote);
return;
}
relay_remote_network_read_websocket_frames (remote, frames, num_frames);
for (i = 0; i < num_frames; i++)
{
free (frames[i].payload);
}
free (frames);
}
}
/*
* Callback for fd hook.
*/
int
relay_remote_network_recv_cb (const void *pointer, void *data, int fd)
{
struct t_relay_remote *remote;
static char buffer[4096 + 2];
int num_read, end_recv;
/* make C compiler happy */
(void) data;
(void) fd;
remote = (struct t_relay_remote *)pointer;
if (!remote)
return WEECHAT_RC_ERROR;
if (remote->sock < 0)
return WEECHAT_RC_OK;
end_recv = 0;
while (!end_recv)
{
end_recv = 1;
if (remote->tls)
{
if (!remote->gnutls_sess)
return WEECHAT_RC_ERROR;
num_read = gnutls_record_recv (remote->gnutls_sess, buffer,
sizeof (buffer) - 2);
}
else
{
num_read = recv (remote->sock, buffer, sizeof (buffer) - 2, 0);
}
if (num_read > 0)
{
buffer[num_read] = '\0';
if (remote->tls
&& (gnutls_record_check_pending (remote->gnutls_sess) > 0))
{
/*
* if there are unread data in the gnutls buffers,
* go on with recv
*/
end_recv = 0;
}
relay_remote_network_recv_buffer (remote, buffer, num_read);
}
else
{
if (remote->tls)
{
if ((num_read == 0)
|| ((num_read != GNUTLS_E_AGAIN)
&& (num_read != GNUTLS_E_INTERRUPTED)))
{
weechat_printf (
NULL,
_("%sremote[%s]: reading data on socket: error %d %s"),
weechat_prefix ("error"),
remote->name,
num_read,
(num_read == 0) ? _("(connection closed by peer)") :
gnutls_strerror (num_read));
relay_remote_network_disconnect (remote);
}
}
else
{
if ((num_read == 0)
|| ((errno != EAGAIN) && (errno != EWOULDBLOCK)))
{
weechat_printf (
NULL,
_("%sremote[%s]: reading data on socket: error %d %s"),
weechat_prefix ("error"),
remote->name,
errno,
(num_read == 0) ? _("(connection closed by peer)") :
strerror (errno));
relay_remote_network_disconnect (remote);
}
}
}
}
return WEECHAT_RC_OK;
}
/*
* Connects to remote using websocket, with authentication.
*/
void
relay_remote_network_connect_ws_auth (struct t_relay_remote *remote)
{
char *password, *totp_secret, *totp;
char *salt_password, salt[64], str_auth[4096], str_auth_base64[4096];
char str_http[8192], str_totp[128], str_extensions[256];
char hash[512 / 8], hash_hexa[((512 / 8) * 2) + 1];
char ws_key[16], ws_key_base64[64];
int hash_size;
time_t time_now;
relay_remote_set_status (remote, RELAY_STATUS_AUTHENTICATING);
password = NULL;
totp_secret = NULL;
str_auth[0] = '\0';
str_auth_base64[0] = '\0';
str_totp[0] = '\0';
str_extensions[0] = '\0';
password = weechat_string_eval_expression (
weechat_config_string (remote->options[RELAY_REMOTE_OPTION_PASSWORD]),
NULL, NULL, NULL);
if (!password)
goto end;
totp_secret = weechat_string_eval_expression (
weechat_config_string (remote->options[RELAY_REMOTE_OPTION_TOTP_SECRET]),
NULL, NULL, NULL);
if (!totp_secret)
goto end;
time_now = time (NULL);
switch (remote->password_hash_algo)
{
case RELAY_AUTH_PASSWORD_HASH_PLAIN:
snprintf (str_auth, sizeof (str_auth), "plain:%s", password);
break;
case RELAY_AUTH_PASSWORD_HASH_SHA256:
case RELAY_AUTH_PASSWORD_HASH_SHA512:
if (weechat_asprintf (&salt_password, "%ld%s", time_now, password) >= 0)
{
if (weechat_crypto_hash (
salt_password, strlen (salt_password),
relay_auth_password_hash_algo_name[remote->password_hash_algo],
hash, &hash_size))
{
weechat_string_base_encode ("16", hash, hash_size, hash_hexa);
snprintf (str_auth, sizeof (str_auth),
"hash:%s:%ld:%s",
relay_auth_password_hash_algo_name[remote->password_hash_algo],
time_now,
hash_hexa);
}
free (salt_password);
}
break;
case RELAY_AUTH_PASSWORD_HASH_PBKDF2_SHA256:
case RELAY_AUTH_PASSWORD_HASH_PBKDF2_SHA512:
snprintf (salt, sizeof (salt), "%ld", time_now);
if (weechat_crypto_hash_pbkdf2 (
password,
strlen (password),
relay_auth_password_hash_algo_name[remote->password_hash_algo] + 7,
salt, strlen (salt),
remote->password_hash_iterations,
hash, &hash_size))
{
weechat_string_base_encode ("16", hash, hash_size, hash_hexa);
snprintf (str_auth, sizeof (str_auth),
"hash:%s:%s:%d:%s",
relay_auth_password_hash_algo_name[remote->password_hash_algo],
salt,
remote->password_hash_iterations,
hash_hexa);
}
break;
}
if (password[0] && !str_auth[0])
{
weechat_printf (NULL,
_("%sremote[%s]: failed to build authentication"),
weechat_prefix ("error"), remote->name);
relay_remote_network_disconnect (remote);
goto end;
}
/* generate random websocket key (16 bytes) */
gcry_create_nonce (ws_key, sizeof (ws_key));
weechat_string_base_encode ("64", ws_key, sizeof (ws_key), ws_key_base64);
free (remote->websocket_key);
remote->websocket_key = strdup (ws_key_base64);
if (str_auth[0])
weechat_string_base_encode ("64", str_auth, strlen (str_auth), str_auth_base64);
if (totp_secret && totp_secret[0])
{
/* generate the TOTP with the secret */
totp = weechat_info_get ("totp_generate", totp_secret);
if (totp)
{
snprintf (str_totp, sizeof (str_totp),
"x-weechat-totp: %s\r\n",
totp);
free (totp);
}
}
/* add supported extensions */
if (weechat_config_boolean (relay_config_network_websocket_permessage_deflate))
{
snprintf (str_extensions, sizeof (str_extensions),
"%s",
"Sec-WebSocket-Extensions: permessage-deflate; "
"client_max_window_bits\r\n");
}
snprintf (
str_http, sizeof (str_http),
"GET /api HTTP/1.1\r\n"
"%s%s%s"
"%s"
"Sec-WebSocket-Version: 13\r\n"
"Sec-WebSocket-Key: %s\r\n"
"Connection: Upgrade\r\n"
"Upgrade: websocket\r\n"
"%s"
"Host: %s:%d\r\n"
"\r\n",
(str_auth_base64[0]) ? "Authorization: Basic " : "",
(str_auth_base64[0]) ? str_auth_base64 : "",
(str_auth_base64[0]) ? "\r\n" : "",
str_totp,
ws_key_base64,
str_extensions,
remote->address,
remote->port);
relay_remote_network_send (remote, RELAY_MSG_STANDARD,
str_http, strlen (str_http));
end:
free (password);
free (totp_secret);
}
/*
* Callback for connect hook.
*/
int
relay_remote_network_connect_cb (const void *pointer, void *data, int status,
int gnutls_rc, int sock, const char *error,
const char *ip_address)
{
struct t_relay_remote *remote;
/* make C compiler happy */
(void) data;
(void) gnutls_rc;
remote = (struct t_relay_remote *)pointer;
remote->hook_connect = NULL;
remote->sock = sock;
switch (status)
{
case WEECHAT_HOOK_CONNECT_OK:
weechat_printf (NULL,
_("remote[%s]: connected to %s/%d (%s)"),
remote->name, remote->address, remote->port,
ip_address);
remote->hook_fd = weechat_hook_fd (remote->sock, 1, 0, 0,
&relay_remote_network_recv_cb,
remote, NULL);
/* authenticate with remote relay */
relay_remote_network_connect_ws_auth (remote);
break;
case WEECHAT_HOOK_CONNECT_ADDRESS_NOT_FOUND:
weechat_printf (NULL,
_("%sremote[%s]: address \"%s\" not found"),
weechat_prefix ("error"), remote->name,
remote->address);
if (error && error[0])
{
weechat_printf (NULL,
_("%sremote[%s]: error: %s"),
weechat_prefix ("error"), remote->name, error);
}
relay_remote_network_disconnect (remote);
break;
case WEECHAT_HOOK_CONNECT_IP_ADDRESS_NOT_FOUND:
weechat_printf (NULL,
_("%sremote[%s]: IP address not found"),
weechat_prefix ("error"), remote->name);
if (error && error[0])
{
weechat_printf (NULL,
_("%sremote[%s]: error: %s"),
weechat_prefix ("error"), remote->name, error);
}
relay_remote_network_disconnect (remote);
break;
case WEECHAT_HOOK_CONNECT_CONNECTION_REFUSED:
weechat_printf (NULL,
_("%sremote[%s]: connection refused"),
weechat_prefix ("error"), remote->name);
if (error && error[0])
{
weechat_printf (NULL,
_("%sremote[%s]: error: %s"),
weechat_prefix ("error"), remote->name, error);
}
relay_remote_network_disconnect (remote);
break;
case WEECHAT_HOOK_CONNECT_PROXY_ERROR:
weechat_printf (
NULL,
_("%sremote[%s]: proxy fails to establish connection to server (check "
"username/password if used and if server address/port is "
"allowed by proxy)"),
weechat_prefix ("error"), remote->name);
if (error && error[0])
{
weechat_printf (NULL,
_("%sremote[%s]: error: %s"),
weechat_prefix ("error"), remote->name, error);
}
relay_remote_network_disconnect (remote);
break;
case WEECHAT_HOOK_CONNECT_LOCAL_HOSTNAME_ERROR:
weechat_printf (NULL,
_("%sremote[%s]: unable to set local hostname/IP"),
weechat_prefix ("error"), remote->name);
if (error && error[0])
{
weechat_printf (NULL,
_("%sremote[%s]: error: %s"),
weechat_prefix ("error"), remote->name, error);
}
relay_remote_network_disconnect (remote);
break;
case WEECHAT_HOOK_CONNECT_GNUTLS_INIT_ERROR:
weechat_printf (NULL,
_("%sremote[%s]: TLS init error"),
weechat_prefix ("error"), remote->name);
if (error && error[0])
{
weechat_printf (NULL,
_("%sremote[%s]: error: %s"),
weechat_prefix ("error"), remote->name, error);
}
relay_remote_network_disconnect (remote);
break;
case WEECHAT_HOOK_CONNECT_GNUTLS_HANDSHAKE_ERROR:
weechat_printf (NULL,
_("%sremote[%s]: TLS handshake failed"),
weechat_prefix ("error"), remote->name);
if (error && error[0])
{
weechat_printf (NULL,
_("%sremote[%s]: error: %s"),
weechat_prefix ("error"), remote->name, error);
}
relay_remote_network_disconnect (remote);
break;
case WEECHAT_HOOK_CONNECT_MEMORY_ERROR:
weechat_printf (NULL,
_("%sremote[%s]: not enough memory"),
weechat_prefix ("error"), remote->name);
if (error && error[0])
{
weechat_printf (NULL,
_("%sremote[%s]: error: %s"),
weechat_prefix ("error"), remote->name, error);
}
relay_remote_network_disconnect (remote);
break;
case WEECHAT_HOOK_CONNECT_TIMEOUT:
weechat_printf (NULL,
_("%sremote[%s]: timeout"),
weechat_prefix ("error"), remote->name);
if (error && error[0])
{
weechat_printf (NULL,
_("%sremote[%s]: error: %s"),
weechat_prefix ("error"), remote->name, error);
}
relay_remote_network_disconnect (remote);
break;
case WEECHAT_HOOK_CONNECT_SOCKET_ERROR:
weechat_printf (NULL,
_("%sremote[%s]: unable to create socket"),
weechat_prefix ("error"), remote->name);
if (error && error[0])
{
weechat_printf (NULL,
_("%sremote[%s]: error: %s"),
weechat_prefix ("error"), remote->name, error);
}
relay_remote_network_disconnect (remote);
break;
}
return WEECHAT_RC_OK;
}
/*
* GnuTLS callback called during handshake.
*
* Returns:
* 0: certificate OK
* -1: error in certificate
*/
int
relay_remote_network_gnutls_callback (const void *pointer, void *data,
gnutls_session_t tls_session,
const gnutls_datum_t *req_ca, int nreq,
const gnutls_pk_algorithm_t *pk_algos,
int pk_algos_len,
gnutls_retr2_st *answer,
int action)
{
struct t_relay_remote *remote;
gnutls_x509_crt_t cert_temp;
const gnutls_datum_t *cert_list;
unsigned int i, cert_list_len, status;
time_t cert_time;
int rc, hostname_match, cert_temp_init;
gnutls_datum_t cinfo;
int rinfo;
/* make C compiler happy */
(void) data;
(void) req_ca;
(void) nreq;
(void) pk_algos;
(void) pk_algos_len;
(void) answer;
rc = 0;
if (!pointer)
return -1;
remote = (struct t_relay_remote *) pointer;
cert_temp_init = 0;
cert_list = NULL;
cert_list_len = 0;
if (action == WEECHAT_HOOK_CONNECT_GNUTLS_CB_VERIFY_CERT)
{
/* initialize the certificate structure */
if (gnutls_x509_crt_init (&cert_temp) != GNUTLS_E_SUCCESS)
{
weechat_printf_date_tags (
NULL, 0, "tls",
_("%sremote[%s]: gnutls: failed to initialize certificate structure"),
weechat_prefix ("error"), remote->name);
rc = -1;
goto end;
}
/* flag to do the "deinit" (at the end of function) */
cert_temp_init = 1;
/* set match options */
hostname_match = 0;
/* get the peer's raw certificate (chain) as sent by the peer */
cert_list = gnutls_certificate_get_peers (tls_session, &cert_list_len);
if (cert_list)
{
weechat_printf_date_tags (
NULL, 0, "tls",
NG_("remote[%s]: gnutls: receiving %d certificate",
"remote[%s]: gnutls: receiving %d certificates",
cert_list_len),
remote->name,
cert_list_len);
for (i = 0; i < cert_list_len; i++)
{
if (gnutls_x509_crt_import (cert_temp,
&cert_list[i],
GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS)
{
weechat_printf_date_tags (
NULL, 0, "tls",
_("%sremote[%s]: gnutls: failed to import certificate[%d]"),
weechat_prefix ("error"), remote->name, i + 1);
rc = -1;
goto end;
}
/* checks on first certificate received */
if (i == 0)
{
/* check if hostname matches in the first certificate */
if (gnutls_x509_crt_check_hostname (cert_temp,
remote->address) != 0)
{
hostname_match = 1;
}
}
/* display infos about certificate */
rinfo = gnutls_x509_crt_print (cert_temp,
GNUTLS_CRT_PRINT_ONELINE, &cinfo);
if (rinfo == 0)
{
weechat_printf_date_tags (
NULL, 0, "tls",
_("remote[%s] - certificate[%d] info:"),
remote->name, i + 1);
weechat_printf_date_tags (
NULL, 0, "tls",
"remote[%s] - %s",
remote->name, cinfo.data);
gnutls_free (cinfo.data);
}
/* check expiration date */
cert_time = gnutls_x509_crt_get_expiration_time (cert_temp);
if (cert_time < time (NULL))
{
weechat_printf_date_tags (
NULL, 0, "tls",
_("%sremote[%s]: gnutls: certificate has expired"),
weechat_prefix ("error"), remote->name);
rc = -1;
}
/* check activation date */
cert_time = gnutls_x509_crt_get_activation_time (cert_temp);
if (cert_time > time (NULL))
{
weechat_printf_date_tags (
NULL, 0, "tls",
_("%sremote[%s]: gnutls: certificate is not yet activated"),
weechat_prefix ("error"), remote->name);
rc = -1;
}
}
if (!hostname_match)
{
weechat_printf_date_tags (
NULL, 0, "tls",
_("%sremote[%s]: gnutls: the hostname in the certificate "
"does NOT match \"%s\""),
weechat_prefix ("error"), remote->name, remote->address);
rc = -1;
}
}
/* verify the peers certificate */
if (gnutls_certificate_verify_peers2 (tls_session, &status) < 0)
{
weechat_printf_date_tags (
NULL, 0, "tls",
_("%sremote[%s]: gnutls: error while checking peer's certificate"),
weechat_prefix ("error"), remote->name);
rc = -1;
goto end;
}
/* check if certificate is trusted */
if (status & GNUTLS_CERT_INVALID)
{
weechat_printf_date_tags (
NULL, 0, "tls",
_("%sremote[%s]: gnutls: peer's certificate is NOT trusted"),
weechat_prefix ("error"), remote->name);
rc = -1;
}
else
{
weechat_printf_date_tags (
NULL, 0, "tls",
_("remote[%s]: gnutls: peer's certificate is trusted"),
remote->name);
}
/* check if certificate issuer is known */
if (status & GNUTLS_CERT_SIGNER_NOT_FOUND)
{
weechat_printf_date_tags (
NULL, 0, "tls",
_("%sremote[%s]: gnutls: peer's certificate issuer is unknown"),
weechat_prefix ("error"), remote->name);
rc = -1;
}
/* check that certificate is not revoked */
if (status & GNUTLS_CERT_REVOKED)
{
weechat_printf_date_tags (
NULL, 0, "tls",
_("%sremote[%s]: gnutls: the certificate has been revoked"),
weechat_prefix ("error"), remote->name);
rc = -1;
}
}
else if (action == WEECHAT_HOOK_CONNECT_GNUTLS_CB_SET_CERT)
{
/* nothing here */
}
end:
/* an error should stop the handshake unless the user doesn't care */
if ((rc == -1)
&& !weechat_config_boolean (remote->options[RELAY_REMOTE_OPTION_TLS_VERIFY]))
{
rc = 0;
}
if (cert_temp_init)
gnutls_x509_crt_deinit (cert_temp);
return rc;
}
/*
* Callback for handshake URL.
*/
int
relay_remote_network_url_handshake_cb (const void *pointer,
void *data,
const char *url,
struct t_hashtable *options,
struct t_hashtable *output)
{
struct t_relay_remote *remote;
struct t_config_option *proxy_type, *proxy_ipv6, *proxy_address, *proxy_port;
const char *ptr_output, *ptr_resp_code, *ptr_error;
const char *proxy, *str_proxy_type, *str_proxy_address;
char *option_name;
cJSON *json_body, *json_hash_algo, *json_hash_iterations, *json_totp;
int length;
/* make C compiler happy */
(void) data;
(void) url;
(void) options;
remote = (struct t_relay_remote *)pointer;
remote->hook_url_handshake = NULL;
ptr_resp_code = weechat_hashtable_get (output, "response_code");
if (ptr_resp_code && ptr_resp_code[0] && (strcmp (ptr_resp_code, "200") != 0))
{
weechat_printf (
NULL,
_("%sremote[%s]: handshake failed with URL %s, response code: %s"),
weechat_prefix ("error"),
remote->name,
weechat_config_string (remote->options[RELAY_REMOTE_OPTION_URL]),
ptr_resp_code);
relay_remote_network_disconnect (remote);
return WEECHAT_RC_OK;
}
ptr_error = weechat_hashtable_get (output, "error");
if (ptr_error && ptr_error[0])
{
weechat_printf (
NULL,
_("%sremote[%s]: handshake failed with URL %s, error: %s"),
weechat_prefix ("error"),
remote->name,
weechat_config_string (remote->options[RELAY_REMOTE_OPTION_URL]),
ptr_error);
relay_remote_network_disconnect (remote);
return WEECHAT_RC_OK;
}
ptr_output = weechat_hashtable_get (output, "output");
if (ptr_output && ptr_output[0])
{
json_body = cJSON_Parse (weechat_hashtable_get (output, "output"));
if (json_body)
{
/* hash algorithm */
json_hash_algo = cJSON_GetObjectItem (json_body, "password_hash_algo");
if (json_hash_algo && cJSON_IsString (json_hash_algo))
{
remote->password_hash_algo = relay_auth_password_hash_algo_search (
cJSON_GetStringValue (json_hash_algo));
}
/* hash iterations */
json_hash_iterations = cJSON_GetObjectItem (json_body, "password_hash_iterations");
if (json_hash_iterations && cJSON_IsNumber (json_hash_iterations))
remote->password_hash_iterations = (int)cJSON_GetNumberValue (json_hash_iterations);
/* TOTP */
json_totp = cJSON_GetObjectItem (json_body, "totp");
if (json_totp && cJSON_IsBool (json_totp))
remote->totp = (cJSON_IsTrue (json_totp)) ? 1 : 0;
}
}
if (remote->password_hash_algo < 0)
{
weechat_printf (
NULL,
_("%sremote[%s]: handshake failed with URL %s, error: %s"),
weechat_prefix ("error"),
remote->name,
weechat_config_string (remote->options[RELAY_REMOTE_OPTION_URL]),
_("hash algorithm not found"));
relay_remote_network_disconnect (remote);
return WEECHAT_RC_OK;
}
if (remote->password_hash_iterations < 0)
{
weechat_printf (
NULL,
_("%sremote[%s]: handshake failed with URL %s, error: %s"),
weechat_prefix ("error"),
remote->name,
weechat_config_string (remote->options[RELAY_REMOTE_OPTION_URL]),
_("unknown number of hash iterations"));
relay_remote_network_disconnect (remote);
return WEECHAT_RC_OK;
}
if (remote->totp < 0)
{
weechat_printf (
NULL,
_("%sremote[%s]: handshake failed with URL %s, error: %s"),
weechat_prefix ("error"),
remote->name,
weechat_config_string (remote->options[RELAY_REMOTE_OPTION_URL]),
_("unknown TOTP status"));
relay_remote_network_disconnect (remote);
return WEECHAT_RC_OK;
}
if (weechat_relay_plugin->debug >= 1)
{
weechat_printf (
NULL,
_("%sremote[%s]: successful handshake with URL %s: "
"hash_algo=%s, iterations=%d, totp=%d"),
RELAY_PLUGIN_NAME,
remote->name,
weechat_config_string (remote->options[RELAY_REMOTE_OPTION_URL]),
relay_auth_password_hash_algo_name[remote->password_hash_algo],
remote->password_hash_iterations,
remote->totp);
}
proxy_type = NULL;
proxy_ipv6 = NULL;
proxy_address = NULL;
proxy_port = NULL;
str_proxy_type = NULL;
str_proxy_address = NULL;
proxy = weechat_config_string (remote->options[RELAY_REMOTE_OPTION_PROXY]);
if (proxy && proxy[0])
{
length = 32 + strlen (proxy) + 1;
option_name = malloc (length);
if (!option_name)
{
weechat_printf (
NULL,
_("%sremote[%s]: not enough memory"),
weechat_prefix ("error"),
remote->name);
relay_remote_network_disconnect (remote);
return WEECHAT_RC_OK;
}
snprintf (option_name, length, "weechat.proxy.%s.type", proxy);
proxy_type = weechat_config_get (option_name);
snprintf (option_name, length, "weechat.proxy.%s.ipv6", proxy);
proxy_ipv6 = weechat_config_get (option_name);
snprintf (option_name, length, "weechat.proxy.%s.address", proxy);
proxy_address = weechat_config_get (option_name);
snprintf (option_name, length, "weechat.proxy.%s.port", proxy);
proxy_port = weechat_config_get (option_name);
free (option_name);
if (!proxy_type || !proxy_address)
{
weechat_printf (
NULL,
_("%sremote[%s]: proxy \"%s\" not found, cannot connect"),
weechat_prefix ("error"), remote->name, proxy);
relay_remote_network_disconnect (remote);
return WEECHAT_RC_OK;
}
str_proxy_type = weechat_config_string (proxy_type);
str_proxy_address = weechat_config_string (proxy_address);
if (!str_proxy_type[0] || !proxy_ipv6 || !str_proxy_address[0]
|| !proxy_port)
{
weechat_printf (
NULL,
_("%sremote[%s]: missing proxy settings, check options for "
"proxy \"%s\""),
weechat_prefix ("error"), remote->name, proxy);
relay_remote_network_disconnect (remote);
return WEECHAT_RC_OK;
}
}
remote->hook_connect = weechat_hook_connect (
proxy,
remote->address,
remote->port,
(proxy_type) ? weechat_config_integer (proxy_ipv6) : 1, /* IPv6 */
0, /* retry */
(remote->tls) ? &remote->gnutls_sess : NULL,
(remote->tls) ? &relay_remote_network_gnutls_callback : NULL,
2048, /* gnutls_dhkey_size */
"NORMAL", /* gnutls_priorities */
NULL, /* local_hostname */
&relay_remote_network_connect_cb,
remote,
NULL);
return WEECHAT_RC_OK;
}
/*
* Builds a string with the API HTTP handshake request.
*
* Note: result must be freed after use.
*/
char *
relay_remote_network_get_handshake_request (void)
{
cJSON *json, *json_algos;
char *result;
int i;
json = cJSON_CreateObject ();
if (!json)
return NULL;
json_algos = cJSON_CreateArray ();
if (!json_algos)
{
cJSON_Delete (json);
return NULL;
}
/* all password hash algorithms are supported */
for (i = 0; i < RELAY_NUM_PASSWORD_HASH_ALGOS; i++)
{
cJSON_AddItemToArray (
json_algos,
cJSON_CreateString (relay_auth_password_hash_algo_name[i]));
}
cJSON_AddItemToObject (json, "password_hash_algo", json_algos);
result = cJSON_PrintUnformatted (json);
cJSON_Delete (json);
return result;
}
/*
* Connects to a remote WeeChat relay/api.
*
* Returns:
* 1: OK
* 0: error
*/
int
relay_remote_network_connect (struct t_relay_remote *remote)
{
char *url, *body;
struct t_hashtable *options;
url = NULL;
body = NULL;
options = NULL;
if (!remote)
return 0;
if (remote->sock != -1)
{
weechat_printf (
NULL,
_("%s%s: already connected to remote relay \"%s\"!"),
weechat_prefix ("error"), RELAY_PLUGIN_NAME, remote->name);
return 0;
}
relay_remote_set_status (remote, RELAY_STATUS_CONNECTING);
weechat_printf (NULL,
_("remote[%s]: connecting to remote relay %s/%d%s..."),
remote->name,
remote->address,
remote->port,
(remote->tls) ? " (TLS)" : "");
url = relay_remote_network_get_url_resource (remote, "handshake");
if (!url)
goto error;
options = weechat_hashtable_new (32,
WEECHAT_HASHTABLE_STRING,
WEECHAT_HASHTABLE_STRING,
NULL, NULL);
if (!options)
goto error;
weechat_hashtable_set (options, "post", "1");
weechat_hashtable_set (options,
"httpheader",
"Accept: application/json\n"
"Content-Type: application/json; charset=utf-8");
if (!weechat_config_boolean (remote->options[RELAY_REMOTE_OPTION_TLS_VERIFY]))
{
weechat_hashtable_set (options, "ssl_verifypeer", "0");
weechat_hashtable_set (options, "ssl_verifyhost", "0");
}
body = relay_remote_network_get_handshake_request ();
if (!body)
goto error;
weechat_hashtable_set (options, "postfields", body);
remote->hook_url_handshake = weechat_hook_url (
url, options, 5 * 1000,
&relay_remote_network_url_handshake_cb, remote, NULL);
free (url);
free (body);
weechat_hashtable_free (options);
return 1;
error:
weechat_printf (NULL,
_("remote[%s]: failed to connect, not enough memory"),
remote->name);
free (url);
free (body);
weechat_hashtable_free (options);
return 0;
}