1
0
mirror of https://github.com/weechat/weechat.git synced 2026-06-27 21:36:37 +02:00
Files
weechat/src/plugins/relay/relay-http.c
T

1762 lines
50 KiB
C

/*
* SPDX-FileCopyrightText: 2023-2026 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/>.
*/
/* HTTP request parser for relay plugin */
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <zlib.h>
#ifdef HAVE_ZSTD
#include <zstd.h>
#endif
#include "../weechat-plugin.h"
#include "relay.h"
#include "relay-auth.h"
#include "relay-client.h"
#include "relay-config.h"
#include "relay-http.h"
#include "relay-raw.h"
#include "relay-websocket.h"
#ifdef HAVE_CJSON
#include "api/relay-api.h"
#endif
#define HEX2DEC(c) (((c >= 'a') && (c <= 'f')) ? c - 'a' + 10 : \
((c >= 'A') && (c <= 'F')) ? c - 'A' + 10 : \
c - '0')
/*
* Reinitialize the HTTP request status.
*/
void
relay_http_request_reinit (struct t_relay_http_request *request)
{
request->status = RELAY_HTTP_METHOD;
weechat_string_dyn_copy (request->raw, NULL);
free (request->method);
request->method = NULL;
free (request->path);
request->path = NULL;
weechat_string_free_split (request->path_items);
request->path_items = NULL;
request->num_path_items = 0;
weechat_hashtable_remove_all (request->params);
free (request->http_version);
request->http_version = NULL;
weechat_hashtable_remove_all (request->headers);
weechat_hashtable_remove_all (request->accept_encoding);
relay_websocket_deflate_free (request->ws_deflate);
request->ws_deflate = relay_websocket_deflate_alloc ();
request->content_length = 0;
request->body_size = 0;
free (request->body);
request->body = NULL;
free (request->id);
request->id = NULL;
}
/*
* Allocate a t_relay_http_request structure.
*/
struct t_relay_http_request *
relay_http_request_alloc (void)
{
struct t_relay_http_request *new_request;
new_request = (struct t_relay_http_request *)malloc (sizeof (*new_request));
if (!new_request)
return NULL;
new_request->status = RELAY_HTTP_METHOD;
new_request->raw = weechat_string_dyn_alloc (64);
new_request->method = NULL;
new_request->path = NULL;
new_request->path_items = NULL;
new_request->num_path_items = 0;
new_request->params = weechat_hashtable_new (
32,
WEECHAT_HASHTABLE_STRING,
WEECHAT_HASHTABLE_STRING,
NULL, NULL);
new_request->http_version = NULL;
new_request->headers = weechat_hashtable_new (
32,
WEECHAT_HASHTABLE_STRING,
WEECHAT_HASHTABLE_STRING,
NULL, NULL);
new_request->accept_encoding = weechat_hashtable_new (
32,
WEECHAT_HASHTABLE_STRING,
WEECHAT_HASHTABLE_STRING,
NULL, NULL);
new_request->ws_deflate = relay_websocket_deflate_alloc ();
new_request->content_length = 0;
new_request->body_size = 0;
new_request->body = NULL;
new_request->id = NULL;
return new_request;
}
/*
* Decode URL: replace "%" chars by their values (eg: "%23" -> "#").
*/
char *
relay_http_url_decode (const char *url)
{
const char *ptr_url, *ptr_next;
char **out, str_char[2];
int length;
if (!url)
return NULL;
length = strlen (url);
out = weechat_string_dyn_alloc ((length > 0) ? length : 1);
if (!out)
return NULL;
ptr_url = url;
while (ptr_url && ptr_url[0])
{
if ((ptr_url[0] == '%')
&& (isxdigit ((unsigned char)ptr_url[1]))
&& (isxdigit ((unsigned char)ptr_url[2])))
{
snprintf (str_char, sizeof (str_char),
"%c",
(HEX2DEC(ptr_url[1]) << 4) + HEX2DEC(ptr_url[2]));
weechat_string_dyn_concat (out, str_char, -1);
ptr_url += 3;
}
else
{
ptr_next = weechat_utf8_next_char (ptr_url);
weechat_string_dyn_concat (
out,
ptr_url,
(ptr_next) ? ptr_next - ptr_url : -1);
ptr_url = ptr_next;
}
}
return weechat_string_dyn_free (out, 0);
}
/*
* Read value of a query string parameter as boolean (0 or 1) into *value.
* If the parameter is not in URL, use the default value.
*
* Return:
* 1: OK, *value is set
* 0: error (query string parameter has invalid format)
*/
int
relay_http_get_param_boolean (struct t_relay_http_request *request,
const char *name, int default_value,
int *value)
{
const char *ptr_value;
if (!value)
return 0;
ptr_value = weechat_hashtable_get (request->params, name);
if (ptr_value && !ptr_value[0])
return 0;
*value = (ptr_value) ?
weechat_config_string_to_boolean (ptr_value) : default_value;
return 1;
}
/*
* Read value of a query string parameter as long into *value.
* If the parameter is not in URL, use the default value.
*
* Return:
* 1: OK, *value is set
* 0: error (URL parameter has invalid format)
*/
int
relay_http_get_param_long (struct t_relay_http_request *request,
const char *name, long default_value,
long *value)
{
const char *ptr_value;
char *error;
long number;
if (!value)
return 0;
ptr_value = weechat_hashtable_get (request->params, name);
if (ptr_value && !ptr_value[0])
return 0;
if (ptr_value)
{
number = strtol (ptr_value, &error, 10);
if (!error || error[0])
return 0;
*value = number;
}
else
{
*value = default_value;
}
return 1;
}
/*
* Get decoded path items from path.
*/
void
relay_http_parse_path (const char *path,
char ***paths, int *num_paths,
struct t_hashtable *params)
{
char *pos, *str_path, *str_params, **items_path, **items2_path;
char **items_params, *name, *value;
int i, num_items_path, num_items_params;
*paths = NULL;
*num_paths = 0;
if (!path)
return;
str_path = NULL;
str_params = NULL;
items_path = NULL;
items2_path = NULL;
pos = strchr (path, '?');
if (pos)
{
str_path = weechat_strndup (path, pos - path);
str_params = strdup (pos + 1);
}
else
{
str_path = strdup (path);
}
/*
* decode path items (until '?' or end of string):
* "/path/to/irc.libera.%23weechat"
* => ["path", "to", "irc.libera.#weechat"]
*/
items_path = weechat_string_split (
(str_path[0] == '/') ? str_path + 1 : str_path,
"/",
NULL,
WEECHAT_STRING_SPLIT_STRIP_LEFT
| WEECHAT_STRING_SPLIT_STRIP_RIGHT
| WEECHAT_STRING_SPLIT_COLLAPSE_SEPS,
0, &num_items_path);
if (items_path && (num_items_path > 0))
{
items2_path = malloc (sizeof (*items_path) * (num_items_path + 1));
if (items2_path)
{
for (i = 0; i < num_items_path; i++)
{
items2_path[i] = relay_http_url_decode (items_path[i]);
}
items2_path[num_items_path] = NULL;
}
*paths = items2_path;
*num_paths = num_items_path;
}
/*
* decode parameters (starting after '?'):
* "/path/to/irc.libera.%23weechat?option=2&bool=off&fields=a,b,c"
* => {"option": "2", "bool": "off", "fields": "a,b,c"}
*/
if (str_params)
{
items_params = weechat_string_split (str_params, "&", NULL, 0, 0,
&num_items_params);
if (items_params && (num_items_params > 0))
{
for (i = 0; i < num_items_params; i++)
{
pos = strchr (items_params[i], '=');
if (pos)
{
name = weechat_strndup (items_params[i], pos - items_params[i]);
value = relay_http_url_decode (pos + 1);
}
else
{
name = strdup (items_params[i]);
value = strdup ("");
}
if (params)
weechat_hashtable_set (params, name, value);
free (name);
free (value);
}
}
}
free (str_path);
free (str_params);
weechat_string_free_split (items_path);
}
/*
* Parse and save method and path.
*
* Return:
* 1: OK, method and path saved
* 0: error: invalid format
*/
int
relay_http_parse_method_path (struct t_relay_http_request *request,
const char *method_path)
{
char **items;
int num_items;
if (!request || !method_path || !method_path[0])
return 0;
weechat_string_dyn_concat (request->raw, method_path, -1);
weechat_string_dyn_concat (request->raw, "\n", -1);
items = weechat_string_split (method_path, " ", NULL,
WEECHAT_STRING_SPLIT_STRIP_LEFT
| WEECHAT_STRING_SPLIT_STRIP_RIGHT
| WEECHAT_STRING_SPLIT_COLLAPSE_SEPS,
0, &num_items);
if (!items || (num_items < 2))
goto error;
free (request->method);
request->method = strdup (items[0]);
free (request->path);
request->path = strdup (items[1]);
if (num_items > 2)
{
free (request->http_version);
request->http_version = strdup (items[2]);
}
relay_http_parse_path (request->path,
&(request->path_items),
&(request->num_path_items),
request->params);
request->status = RELAY_HTTP_HEADERS;
weechat_string_free_split (items);
return 1;
error:
weechat_string_free_split (items);
request->status = RELAY_HTTP_END;
return 0;
}
/*
* Parse and save a HTTP header in hashtable "headers".
*
* The parameter "ws_deflate_allowed" controls whether the websocket extension
* "permessage-deflate" is allowed or not (it is allowed only with "api"
* protocol).
*
* Return:
* 1: OK, header saved
* 0: error: invalid format
*/
int
relay_http_parse_header (struct t_relay_http_request *request,
const char *header,
int ws_deflate_allowed)
{
char *pos, *name, *name_lower, *error, **items;
const char *existing_value, *ptr_value;
int i, num_items;
long number;
weechat_string_dyn_concat (request->raw, header, -1);
weechat_string_dyn_concat (request->raw, "\n", -1);
/* empty line => end of headers */
if (!header || !header[0])
{
request->status = (request->content_length > 0) ?
RELAY_HTTP_BODY : RELAY_HTTP_END;
return 1;
}
pos = strchr (header, ':');
/* not a valid header */
if (!pos || (pos == header))
return 0;
/* get header name, which is case-insensitive */
name = weechat_strndup (header, pos - header);
if (!name)
return 0;
name_lower = weechat_string_tolower (name);
if (!name_lower)
{
free (name);
return 0;
}
/* get pointer on header value */
ptr_value = pos + 1;
while (ptr_value[0] == ' ')
{
ptr_value++;
}
existing_value = weechat_hashtable_get (request->headers, name_lower);
if (existing_value)
ptr_value = WEECHAT_STR_CONCAT(", ", existing_value, ptr_value);
/* add header in the hashtable */
weechat_hashtable_set (request->headers, name_lower, ptr_value);
/* if header is "Accept-Encoding", save the allowed encoding */
if (strcmp (name_lower, "accept-encoding") == 0)
{
items = weechat_string_split (ptr_value, ",", " ", 0, 0, &num_items);
if (items)
{
for (i = 0; i < num_items; i++)
{
weechat_hashtable_set (request->accept_encoding, items[i], NULL);
}
weechat_string_free_split (items);
}
}
/* if header is "Content-Length", save the length */
if (strcmp (name_lower, "content-length") == 0)
{
error = NULL;
number = strtol (ptr_value, &error, 10);
if (error && !error[0])
request->content_length = (int)number;
}
/*
* if header is "Sec-WebSocket-Extensions", save supported websocket
* extensions
*/
if (strcmp (name_lower, "sec-websocket-extensions") == 0)
{
relay_websocket_parse_extensions (
ptr_value,
request->ws_deflate,
ws_deflate_allowed);
}
free (name);
free (name_lower);
return 1;
}
/*
* Add bytes to HTTP body, change the status to RELAY_HTTP_END if the body
* is complete.
*/
void
relay_http_add_to_body (struct t_relay_http_request *request,
char **partial_message)
{
char *new_body, *new_partial;
int num_bytes_missing, length_msg, length;
if (!partial_message || !*partial_message)
return;
/*
* reject the body if its announced length is too big: this prevents a
* client from forcing an unbounded allocation by announcing a huge
* "Content-Length"
*/
if (request->content_length > RELAY_HTTP_BODY_MAX_LENGTH)
{
free (*partial_message);
*partial_message = NULL;
request->status = RELAY_HTTP_END;
return;
}
num_bytes_missing = request->content_length
- request->body_size;
if (num_bytes_missing <= 0)
{
request->status = RELAY_HTTP_END;
return;
}
length_msg = strlen (*partial_message);
if (num_bytes_missing >= length_msg)
{
new_body = realloc (
request->body,
request->body_size + length_msg + 1);
if (new_body)
{
request->body = new_body;
memcpy (request->body + request->body_size,
*partial_message,
length_msg);
request->body[request->body_size + length_msg] = '\0';
request->body_size += length_msg;
weechat_string_dyn_concat (request->raw, *partial_message, -1);
}
free (*partial_message);
*partial_message = NULL;
}
else
{
new_body = realloc (
request->body,
request->body_size + num_bytes_missing + 1);
if (new_body)
{
request->body = new_body;
memcpy (request->body + request->body_size,
*partial_message,
num_bytes_missing);
request->body[request->body_size + num_bytes_missing] = '\0';
request->body_size += num_bytes_missing;
weechat_string_dyn_concat (request->raw,
*partial_message, num_bytes_missing);
length = strlen (*partial_message + num_bytes_missing);
new_partial = malloc (length + 1);
if (new_partial)
{
memcpy (new_partial,
*partial_message + num_bytes_missing,
length + 1);
free (*partial_message);
*partial_message = new_partial;
}
}
}
if (request->body_size >= request->content_length)
request->status = RELAY_HTTP_END;
}
/*
* Get authentication status according to headers in the request.
*
* Return:
* 0: authentication OK (password + TOTP if enabled)
* -1: missing password
* -2: invalid password
* -3: missing TOTP
* -4: invalid TOTP
* -5: invalid hash algorithm
* -6: invalid timestamp (used as salt)
* -7: invalid number of iterations (PBKDF2)
* -8: out of memory
*/
int
relay_http_get_auth_status (struct t_relay_client *client)
{
const char *auth, *sec_websocket_protocol, *client_totp, *pos;
char *relay_password, *totp_secret, *info_totp_args, *info_totp;
char *user_pass, **protocol_array, *error;
int rc, i, length, protocol_count, use_base64url, totp_ok;
long number;
rc = 0;
relay_password = NULL;
protocol_array = NULL;
totp_secret = NULL;
user_pass = NULL;
use_base64url = 0;
/* check TOTP */
client_totp = weechat_hashtable_get (client->http_req->headers, "x-weechat-totp");
if (client_totp)
{
if (!client_totp[0])
{
rc = -4;
goto end;
}
number = strtol (client_totp, &error, 10);
if (!error || error[0] || (number < 0) || (number > 999999))
{
rc = -4;
goto end;
}
}
totp_secret = weechat_string_eval_expression (
weechat_config_string (relay_config_network_totp_secret),
NULL, NULL, NULL);
if (totp_secret && totp_secret[0])
{
if (!client_totp || !client_totp[0])
{
rc = -3;
goto end;
}
/* validate the TOTP received from the client */
if (weechat_asprintf (
&info_totp_args,
"%s,%s,0,%d",
totp_secret, /* the shared secret */
client_totp, /* the TOTP from client */
weechat_config_integer (relay_config_network_totp_window)) >= 0)
{
info_totp = weechat_info_get ("totp_validate", info_totp_args);
totp_ok = (info_totp && (strcmp (info_totp, "1") == 0)) ?
1 : 0;
free (info_totp);
free (info_totp_args);
if (!totp_ok)
{
rc = -4;
goto end;
}
}
}
else
{
/* error if TOTP received without TOTP configuration */
if (client_totp && client_totp[0])
{
rc = -4;
goto end;
}
}
/* check password */
relay_password = weechat_string_eval_expression (
weechat_config_string (relay_config_network_password),
NULL, NULL, NULL);
if (!relay_password)
{
rc = -8;
goto end;
}
if (!relay_password[0]
&& !weechat_config_boolean (relay_config_network_allow_empty_password))
{
rc = -2;
goto end;
}
if (relay_password[0])
{
auth = weechat_hashtable_get (client->http_req->headers, "authorization");
if (auth)
{
if (weechat_strncasecmp (auth, "basic ", 6) != 0)
{
rc = -1;
goto end;
}
pos = auth + 6;
}
else
{
sec_websocket_protocol = weechat_hashtable_get (
client->http_req->headers, "sec-websocket-protocol");
protocol_array = weechat_string_split (sec_websocket_protocol,
",", " ", 0, 0, &protocol_count);
pos = NULL;
for (i = 0; i < protocol_count; i++)
{
if (strncmp (protocol_array[i],
"base64url.bearer.authorization.weechat.", 39) == 0)
{
pos = protocol_array[i] + 39;
use_base64url = 1;
break;
}
}
if (!pos)
{
rc = -1;
goto end;
}
}
while (pos[0] == ' ')
{
pos++;
}
length = strlen (pos);
user_pass = malloc (length + 1);
if (!user_pass)
{
rc = -8;
goto end;
}
length = weechat_string_base_decode ((use_base64url) ? "64url" : "64",
pos, user_pass);
if (length < 0)
{
rc = -2;
goto end;
}
if (strncmp (user_pass, "plain:", 6) == 0)
{
switch (relay_auth_check_password_plain (client,
user_pass + 6,
relay_password))
{
case 0: /* password OK */
break;
case -1: /* "plain" is not allowed */
rc = -5;
goto end;
case -2: /* invalid password */
default:
rc = -2;
goto end;
}
}
else if (strncmp (user_pass, "hash:", 5) == 0)
{
switch (relay_auth_password_hash (client, user_pass + 5,
relay_password))
{
case 0: /* password OK */
break;
case -1: /* invalid hash algorithm */
rc = -5;
goto end;
case -2: /* invalid timestamp */
rc = -6;
goto end;
case -3: /* invalid iterations */
rc = -7;
goto end;
case -4: /* invalid password */
default:
rc = -2;
goto end;
}
}
else
{
rc = -2;
goto end;
}
}
end:
weechat_string_free_split (protocol_array);
free (relay_password);
free (totp_secret);
free (user_pass);
return rc;
}
/*
* Check authentication in HTTP request.
*
* Return:
* 1: authentication OK
* 0: authentication failed
*/
int
relay_http_check_auth (struct t_relay_client *client)
{
int rc;
rc = relay_http_get_auth_status (client);
switch (rc)
{
case 0: /* authentication OK */
break;
case -1: /* missing password */
relay_http_send_error_json (client, RELAY_HTTP_401_UNAUTHORIZED,
NULL,
RELAY_HTTP_ERROR_MISSING_PASSWORD);
break;
case -2: /* invalid password */
relay_http_send_error_json (client, RELAY_HTTP_401_UNAUTHORIZED,
NULL,
RELAY_HTTP_ERROR_INVALID_PASSWORD);
break;
case -3: /* missing TOTP */
relay_http_send_error_json (client, RELAY_HTTP_401_UNAUTHORIZED,
NULL,
RELAY_HTTP_ERROR_MISSING_TOTP);
break;
case -4: /* invalid TOTP */
relay_http_send_error_json (client, RELAY_HTTP_401_UNAUTHORIZED,
NULL,
RELAY_HTTP_ERROR_INVALID_TOTP);
break;
case -5: /* invalid hash algorithm */
relay_http_send_error_json (client, RELAY_HTTP_401_UNAUTHORIZED,
NULL,
RELAY_HTTP_ERROR_INVALID_HASH_ALGO);
break;
case -6: /* invalid timestamp */
relay_http_send_error_json (client, RELAY_HTTP_401_UNAUTHORIZED,
NULL,
RELAY_HTTP_ERROR_INVALID_TIMESTAMP);
break;
case -7: /* invalid iterations */
relay_http_send_error_json (client, RELAY_HTTP_401_UNAUTHORIZED,
NULL,
RELAY_HTTP_ERROR_INVALID_ITERATIONS);
break;
case -8: /* out of memory */
relay_http_send_error_json (client, RELAY_HTTP_401_UNAUTHORIZED,
NULL,
RELAY_HTTP_ERROR_OUT_OF_MEMORY);
break;
}
return (rc == 0) ? 1 : 0;
}
/*
* Process HTTP websocket request.
*/
void
relay_http_process_websocket (struct t_relay_client *client)
{
const char *ptr_real_ip;
char *handshake;
int rc;
rc = relay_websocket_client_handshake_valid (client->http_req);
if (rc == -1)
{
relay_http_send (client, RELAY_HTTP_400_BAD_REQUEST, NULL, NULL, 0);
if (weechat_relay_plugin->debug >= 1)
{
weechat_printf_date_tags (
NULL, 0, "relay_client",
_("%s%s: invalid websocket handshake received for client %s%s%s"),
weechat_prefix ("error"),
RELAY_PLUGIN_NAME,
RELAY_COLOR_CHAT_CLIENT,
client->desc,
RELAY_COLOR_CHAT);
}
relay_client_set_status (client, RELAY_STATUS_DISCONNECTED);
return;
}
if (rc == -2)
{
relay_http_send (client, RELAY_HTTP_403_FORBIDDEN, NULL, NULL, 0);
if (weechat_relay_plugin->debug >= 1)
{
weechat_printf_date_tags (
NULL, 0, "relay_client",
_("%s%s: origin \"%s\" is not allowed for websocket"),
weechat_prefix ("error"),
RELAY_PLUGIN_NAME,
weechat_hashtable_get (client->http_req->headers, "origin"));
}
relay_client_set_status (client, RELAY_STATUS_DISCONNECTED);
return;
}
/* handshake from client is valid, auth is mandatory for "api" protocol */
if (client->protocol == RELAY_PROTOCOL_API)
{
if (relay_http_check_auth (client))
{
relay_client_set_status (client, RELAY_STATUS_CONNECTED);
}
else
{
relay_client_set_status (client, RELAY_STATUS_AUTH_FAILED);
return;
}
}
handshake = relay_websocket_build_handshake (client->http_req);
if (handshake)
{
relay_client_send (client,
RELAY_MSG_STANDARD,
handshake,
strlen (handshake), NULL);
free (handshake);
client->websocket = RELAY_CLIENT_WEBSOCKET_READY;
memcpy (client->ws_deflate, client->http_req->ws_deflate,
sizeof (*(client->ws_deflate)));
if (client->protocol == RELAY_PROTOCOL_API)
{
/* "api" protocol uses JSON in input/output (multi-line text) */
client->recv_data_type = RELAY_CLIENT_DATA_TEXT_MULTILINE;
client->send_data_type = RELAY_CLIENT_DATA_TEXT_MULTILINE;
}
}
ptr_real_ip = weechat_hashtable_get (
client->http_req->headers, "x-real-ip");
if (ptr_real_ip)
{
free (client->real_ip);
client->real_ip = strdup (ptr_real_ip);
relay_client_set_desc (client);
weechat_printf_date_tags (
NULL, 0, "relay_client",
_("%s: websocket client %s%s%s has real IP "
"address \"%s\""),
RELAY_PLUGIN_NAME,
RELAY_COLOR_CHAT_CLIENT,
client->desc,
RELAY_COLOR_CHAT,
ptr_real_ip);
}
}
/*
* Process HTTP request.
*/
void
relay_http_process_request (struct t_relay_client *client)
{
if (client->http_req->raw)
{
relay_raw_print_client (client, RELAY_MSG_STANDARD,
RELAY_RAW_FLAG_RECV,
*(client->http_req->raw),
strlen (*(client->http_req->raw)) + 1);
}
/* if websocket is initializing */
if (client->websocket == RELAY_CLIENT_WEBSOCKET_INITIALIZING)
{
relay_http_process_websocket (client);
}
else
{
#ifdef HAVE_CJSON
if (client->protocol == RELAY_PROTOCOL_API)
relay_api_recv_http (client);
#endif /* HAVE_CJSON */
}
}
/*
* Read HTTP data from a client.
*/
void
relay_http_recv (struct t_relay_client *client, const char *data, int size)
{
char *new_partial, *pos, **null_char;
int length, ws_deflate_allowed;
null_char = memchr (data, 0, size);
if (client->partial_message)
{
/*
* limit the size of the partial message: once the maximum is reached,
* ignore the extra data (protection against a client sending a huge
* amount of data without any end-of-line and dribbling it, which would
* consume all the memory)
*/
if (strlen (client->partial_message) >= RELAY_HTTP_PARTIAL_MESSAGE_MAX_LENGTH)
return;
new_partial = realloc (client->partial_message,
strlen (client->partial_message) +
strlen (data) + 1);
if (!new_partial)
return;
client->partial_message = new_partial;
strcat (client->partial_message, data);
}
else
{
client->partial_message = strdup (data);
}
while (client->partial_message)
{
if ((client->http_req->status == RELAY_HTTP_METHOD)
|| (client->http_req->status == RELAY_HTTP_HEADERS))
{
pos = strchr (client->partial_message, '\r');
if (!pos)
break;
pos[0] = '\0';
if (client->http_req->status == RELAY_HTTP_METHOD)
{
relay_http_parse_method_path (client->http_req,
client->partial_message);
}
else
{
ws_deflate_allowed = (client->protocol == RELAY_PROTOCOL_API) ?
1 : 0;
relay_http_parse_header (client->http_req,
client->partial_message,
ws_deflate_allowed);
}
pos[0] = '\r';
pos++;
if (pos[0] == '\n')
pos++;
length = strlen (pos);
if (length > 0)
{
new_partial = malloc (length + 1);
if (new_partial)
{
memcpy (new_partial, pos, length + 1);
free (client->partial_message);
client->partial_message = new_partial;
}
}
else
{
free (client->partial_message);
client->partial_message = NULL;
}
}
else if (client->http_req->status == RELAY_HTTP_BODY)
{
relay_http_add_to_body (client->http_req, &(client->partial_message));
}
/*
* process the request if it's ready to be processed (all parsed)
* or if we received a NULL char in the HTTP message (forbidden)
* */
if ((client->http_req->status == RELAY_HTTP_END) || null_char)
{
relay_http_process_request (client);
relay_http_request_reinit (client->http_req);
}
/*
* we continue to process HTTP requests only if websocket is
* initializing or for "api" relay
*/
if ((client->websocket != RELAY_CLIENT_WEBSOCKET_INITIALIZING)
&& (client->protocol != RELAY_PROTOCOL_API))
{
break;
}
}
}
/*
* Compress body of HTTP request with gzip or zstd, if all conditions are met:
* - body is not empty
* - gzip or zstd is allowed by client (header "Accept-Encoding")
* (for zstd, WeeChat must be compiled with zstd support)
* - compression is enabled (option relay.network.compression > 0)
*
* "compressed_size" is set to the compressed size (0 if error).
*
* "http_content_encoding" is a pointer to a string that will be set with
* the HTTP header "Content-Encoding", if the compression is successful
* (for example: "Content-Encoding: gzip").
*
* Return pointer to compressed data or NULL if error.
*
* Note: result must be freed after use.
*/
char *
relay_http_compress (struct t_relay_http_request *request,
const char *data, int data_size,
int *compressed_size,
char *http_content_encoding,
int http_content_encoding_size)
{
int rc, compression, compression_level, comp_deflate, comp_gzip, comp_zstd;
Bytef *dest;
uLongf dest_size;
z_stream strm;
#ifdef HAVE_ZSTD
size_t zstd_comp_size;
#endif
if (!request)
return NULL;
if (compressed_size)
*compressed_size = 0;
if (http_content_encoding)
http_content_encoding[0] = '\0';
if (!data || (data_size <= 0) || !compressed_size
|| !http_content_encoding || (http_content_encoding_size <= 0))
{
return NULL;
}
compression = weechat_config_integer (relay_config_network_compression);
if (compression <= 0)
return NULL;
/*
* compression used by priority if allowed:
* 1. zstd
* 2. deflate
* 3. gzip
*/
comp_deflate = weechat_hashtable_has_key (request->accept_encoding, "deflate");
comp_gzip = weechat_hashtable_has_key (request->accept_encoding, "gzip");
#ifdef HAVE_ZSTD
comp_zstd = weechat_hashtable_has_key (request->accept_encoding, "zstd");
#else
comp_zstd = 0;
#endif /* HAVE_ZSTD */
if (!comp_deflate && !comp_gzip && !comp_zstd)
return NULL;
if (comp_deflate)
comp_gzip = 0;
dest = NULL;
dest_size = 0;
#ifdef HAVE_ZSTD
/* compress with zstd */
if (!dest && comp_zstd)
{
/* convert % to zstd compression level (1-19) */
compression_level = (((compression - 1) * 19) / 100) + 1;
dest_size = ZSTD_compressBound (data_size);
dest = malloc (dest_size);
if (dest)
{
zstd_comp_size = ZSTD_compress(
dest,
dest_size,
(void *)data,
data_size,
compression_level);
if (zstd_comp_size > 0)
{
*compressed_size = zstd_comp_size;
strcat (http_content_encoding, "Content-Encoding: zstd\r\n");
}
else
{
free (dest);
dest = NULL;
}
}
}
#endif /* HAVE_ZSTD */
/* compress with deflate (zlib) or gzip */
if (!dest && (comp_deflate || comp_gzip))
{
/* convert % to zlib compression level (1-9) */
compression_level = (((compression - 1) * 9) / 100) + 1;
dest_size = compressBound (data_size);
dest = malloc (dest_size);
if (dest)
{
memset (&strm, 0, sizeof (strm));
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.avail_in = (uInt)data_size;
strm.next_in = (Bytef *)data;
strm.avail_out = (uInt)dest_size;
strm.next_out = (Bytef *)dest;
rc = deflateInit2 (
&strm,
compression_level,
Z_DEFLATED, /* method */
(comp_gzip) ? 15 + 16 : 15, /* + 16 = gzip instead of zlib */
8, /* memLevel */
Z_DEFAULT_STRATEGY); /* strategy */
if (rc == Z_OK)
{
rc = deflate (&strm, Z_FINISH);
(void) deflateEnd (&strm);
if ((rc == Z_STREAM_END) || (rc == Z_OK))
{
*compressed_size = strm.total_out;
if (comp_deflate)
strcat (http_content_encoding, "Content-Encoding: deflate\r\n");
else if (comp_gzip)
strcat (http_content_encoding, "Content-Encoding: gzip\r\n");
}
else
{
free (dest);
dest = NULL;
}
}
else
{
free (dest);
dest = NULL;
}
}
}
return (char *)dest;
}
/*
* Send a HTTP message to client.
*
* Argument "http" is a HTTP code + message, for example:
* "403 Forbidden".
*
* Return number of bytes sent to client, -1 if error.
*/
int
relay_http_send (struct t_relay_client *client,
int return_code, const char *message,
const char *headers,
const char *body, int body_size)
{
const char *ptr_body;
char str_header[1024], str_content_encoding[256];
char *compressed_body, *http_message, *raw_message;
int length_header, length_msg, num_bytes;
int *ptr_body_size, compressed_body_size;
if (!client || !message || (body_size < 0))
return -1;
str_content_encoding[0] = '\0';
ptr_body = body;
ptr_body_size = &body_size;
compressed_body = relay_http_compress (client->http_req, body, body_size,
&compressed_body_size,
str_content_encoding,
sizeof (str_content_encoding));
if (compressed_body)
{
ptr_body = compressed_body;
ptr_body_size = &compressed_body_size;
}
snprintf (str_header, sizeof (str_header),
"HTTP/1.1 %d %s\r\n"
"%s%s"
"%s"
"Content-Length: %d\r\n"
"\r\n",
return_code,
(message) ? message : "",
(headers) ? headers : "",
(headers && headers[0]) ? "\r\n" : "",
str_content_encoding,
*ptr_body_size);
length_header = strlen (str_header);
if (!ptr_body || (*ptr_body_size <= 0))
{
num_bytes = relay_client_send (client, RELAY_MSG_STANDARD,
str_header, length_header, NULL);
}
else
{
length_msg = length_header + (*ptr_body_size);
http_message = malloc (length_msg + 1);
if (http_message)
{
memcpy (http_message, str_header, length_header);
memcpy (http_message + length_header, ptr_body, *ptr_body_size);
http_message[length_msg] = '\0';
raw_message = NULL;
if (compressed_body)
{
weechat_asprintf (&raw_message,
"%s[%d bytes data]",
str_header,
*ptr_body_size);
}
num_bytes = relay_client_send (client, RELAY_MSG_STANDARD,
http_message, length_msg,
raw_message);
free (raw_message);
free (http_message);
}
else
{
num_bytes = -1;
}
}
free (compressed_body);
return num_bytes;
}
/*
* Send JSON string to client.
*
* Return number of bytes sent to client, -1 if error.
*/
int
relay_http_send_json (struct t_relay_client *client,
int return_code,
const char *message,
const char *headers,
const char *json_string)
{
int num_bytes;
char *headers2;
if (!client || !message)
return -1;
weechat_asprintf (
&headers2,
"%s%s%s",
(headers) ? headers : "",
(headers && headers[0]) ? "\r\n" : "",
"Access-Control-Allow-Origin: *\r\n"
"Content-Type: application/json; charset=utf-8");
num_bytes = relay_http_send (client,
return_code,
message,
headers2,
json_string,
(json_string) ? strlen (json_string) : 0);
free (headers2);
return num_bytes;
}
/*
* Send JSON error to client.
*
* Return number of bytes sent to client, -1 if error.
*/
int
relay_http_send_error_json (struct t_relay_client *client,
int return_code,
const char *message,
const char *headers,
const char *format, ...)
{
int num_bytes;
char *error_msg, *json;
if (!client || !message || !format)
return -1;
weechat_va_format (format);
if (!vbuffer)
return -1;
num_bytes = -1;
error_msg = NULL;
json = NULL;
error_msg = weechat_string_replace (vbuffer, "\"", "\\\"");
if (!error_msg)
goto end;
if (weechat_asprintf (&json, "{\"error\":\"%s\"}", error_msg) < 0)
goto end;
num_bytes = relay_http_send_json (client, return_code, message, headers,
json);
end:
free (vbuffer);
free (error_msg);
free (json);
return num_bytes;
}
/*
* Free a HTTP request.
*/
void
relay_http_request_free (struct t_relay_http_request *request)
{
weechat_string_dyn_free (request->raw, 1);
free (request->method);
free (request->path);
weechat_string_free_split (request->path_items);
weechat_hashtable_free (request->params);
free (request->http_version);
weechat_hashtable_free (request->headers);
weechat_hashtable_free (request->accept_encoding);
relay_websocket_deflate_free (request->ws_deflate);
free (request->body);
free (request->id);
free (request);
}
/*
* Allocate a t_relay_http_response structure.
*/
struct t_relay_http_response *
relay_http_response_alloc (void)
{
struct t_relay_http_response *new_response;
new_response = (struct t_relay_http_response *)malloc (sizeof (*new_response));
if (!new_response)
return NULL;
new_response->status = RELAY_HTTP_METHOD;
new_response->http_version = NULL;
new_response->return_code = 0;
new_response->message = NULL;
new_response->headers = weechat_hashtable_new (
32,
WEECHAT_HASHTABLE_STRING,
WEECHAT_HASHTABLE_STRING,
NULL, NULL);
new_response->content_length = 0;
new_response->body_size = 0;
new_response->body = NULL;
return new_response;
}
/*
* Parse and saves response code.
*
* Return:
* 1: OK, response code and HTTP version saved
* 0: error: invalid format
*/
int
relay_http_parse_response_code (struct t_relay_http_response *response,
const char *response_code)
{
const char *pos, *pos2;
char *error, *return_code;
long value;
if (!response)
return 0;
if (!response_code || !response_code[0])
{
response->status = RELAY_HTTP_END;
return 0;
}
pos = strchr (response_code, ' ');
if (!pos)
goto error;
free (response->http_version);
response->http_version = weechat_strndup (response_code, pos - response_code);
while (pos[0] == ' ')
{
pos++;
}
pos2 = strchr (pos, ' ');
if (pos2)
return_code = weechat_strndup (pos, pos2 - pos);
else
return_code = strdup (pos);
if (!return_code)
goto error;
error = NULL;
value = strtol (return_code, &error, 10);
if (error && !error[0])
response->return_code = (int)value;
free (return_code);
if (pos2)
{
while (pos2[0] == ' ')
{
pos2++;
}
free (response->message);
response->message = strdup (pos2);
}
response->status = RELAY_HTTP_HEADERS;
return 1;
error:
response->status = RELAY_HTTP_END;
return 0;
}
/*
* Parse and saves a header of a HTTP response in hashtable "headers".
*
* Return:
* 1: OK, header saved
* 0: error: invalid format
*/
int
relay_http_parse_response_header (struct t_relay_http_response *response,
const char *header)
{
char *pos, *name, *name_lower, *error;
const char *ptr_value;
long number;
/* empty line => end of headers */
if (!header || !header[0])
{
response->status = (response->content_length > 0) ?
RELAY_HTTP_BODY : RELAY_HTTP_END;
return 1;
}
pos = strchr (header, ':');
/* not a valid header */
if (!pos || (pos == header))
return 0;
/* get header name, which is case-insensitive */
name = weechat_strndup (header, pos - header);
if (!name)
return 0;
name_lower = weechat_string_tolower (name);
if (!name_lower)
{
free (name);
return 0;
}
/* get pointer on header value */
ptr_value = pos + 1;
while (ptr_value[0] == ' ')
{
ptr_value++;
}
/* add header in the hashtable */
weechat_hashtable_set (response->headers, name_lower, ptr_value);
/* if header is "Content-Length", save the length */
if (strcmp (name_lower, "content-length") == 0)
{
error = NULL;
number = strtol (ptr_value, &error, 10);
if (error && !error[0])
response->content_length = (int)number;
}
free (name);
free (name_lower);
return 1;
}
/*
* Parse HTTP response with a string.
*
* Return HTTP request structure, NULL if error.
*/
struct t_relay_http_response *
relay_http_parse_response (const char *data)
{
struct t_relay_http_response *http_resp;
const char *ptr_data, *pos;
char *line;
if (!data || !data[0])
return NULL;
http_resp = relay_http_response_alloc ();
if (!http_resp)
return NULL;
ptr_data = data;
while (ptr_data && ptr_data[0])
{
if ((http_resp->status == RELAY_HTTP_METHOD)
|| (http_resp->status == RELAY_HTTP_HEADERS))
{
pos = strchr (ptr_data, '\r');
if (!pos)
break;
line = weechat_strndup (ptr_data, pos - ptr_data);
if (!line)
break;
if (http_resp->status == RELAY_HTTP_METHOD)
relay_http_parse_response_code (http_resp, line);
else
relay_http_parse_response_header (http_resp, line);
free (line);
ptr_data = pos + 1;
if (ptr_data[0] == '\n')
ptr_data++;
}
else if (http_resp->status == RELAY_HTTP_BODY)
{
http_resp->body_size = strlen (ptr_data);
http_resp->body = malloc (http_resp->body_size + 1);
if (http_resp->body)
{
memcpy (http_resp->body, ptr_data, http_resp->body_size);
http_resp->body[http_resp->body_size] = '\0';
}
http_resp->status = RELAY_HTTP_END;
}
else
break;
if (http_resp->status == RELAY_HTTP_END)
break;
}
return http_resp;
}
/*
* Free a HTTP response.
*/
void
relay_http_response_free (struct t_relay_http_response *response)
{
if (!response)
return;
free (response->http_version);
free (response->message);
weechat_hashtable_free (response->headers);
free (response->body);
free (response);
}
/*
* Print HTTP request in WeeChat log file (usually for crash dump).
*/
void
relay_http_print_log_request (struct t_relay_http_request *request)
{
int i;
weechat_log_printf (" http_request:");
weechat_log_printf (" status. . . . . . . . . : %d", request->status);
weechat_log_printf (" raw . . . . . . . . . . : '%s'",
(request->raw) ? *(request->raw) : NULL);
weechat_log_printf (" method. . . . . . . . . : '%s'", request->method);
weechat_log_printf (" path. . . . . . . . . . : '%s'", request->path);
weechat_log_printf (" path_items. . . . . . . : %p", request->path_items);
if (request->path_items)
{
for (i = 0; request->path_items[i]; i++)
{
weechat_log_printf (" '%s'", request->path_items[i]);
}
}
weechat_log_printf (" num_path_items. . . . . : %d", request->num_path_items);
weechat_log_printf (" params. . . . . . . . . : %p (hashtable: '%s')",
request->params,
weechat_hashtable_get_string (request->params, "keys_values"));
weechat_log_printf (" http_version. . . . . . : '%s'", request->http_version);
weechat_log_printf (" headers . . . . . . . . : %p (hashtable: '%s')",
request->headers,
weechat_hashtable_get_string (request->headers, "keys_values"));
weechat_log_printf (" accept_encoding . . . . : %p (hashtable: '%s')",
request->accept_encoding,
weechat_hashtable_get_string (request->accept_encoding,
"keys_values"));
relay_websocket_deflate_print_log (request->ws_deflate, " ");
weechat_log_printf (" content_length. . . . . : %d", request->content_length);
weechat_log_printf (" body_size . . . . . . . : %d", request->body_size);
weechat_log_printf (" body. . . . . . . . . . : '%s'", request->body);
weechat_log_printf (" id. . . . . . . . . . . : '%s'", request->id);
}
/*
* Print HTTP response in WeeChat log file (usually for crash dump).
*/
void
relay_http_print_log_response (struct t_relay_http_response *response)
{
weechat_log_printf (" http_response:");
weechat_log_printf (" status. . . . . . . . . : %d", response->status);
weechat_log_printf (" http_version. . . . . . : '%s'", response->http_version);
weechat_log_printf (" return_code . . . . . . : %d", response->return_code);
weechat_log_printf (" message . . . . . . . . : '%s'", response->message);
weechat_log_printf (" headers . . . . . . . . : %p (hashtable: '%s')",
response->headers,
weechat_hashtable_get_string (response->headers, "keys_values"));
weechat_log_printf (" content_length. . . . . : %d", response->content_length);
weechat_log_printf (" body_size . . . . . . . : %d", response->body_size);
weechat_log_printf (" body. . . . . . . . . . : '%s'", response->body);
}