diff options
Diffstat (limited to 'src/libserver/http')
-rw-r--r-- | src/libserver/http/http_connection.c | 2649 | ||||
-rw-r--r-- | src/libserver/http/http_connection.h | 320 | ||||
-rw-r--r-- | src/libserver/http/http_context.c | 670 | ||||
-rw-r--r-- | src/libserver/http/http_context.h | 122 | ||||
-rw-r--r-- | src/libserver/http/http_message.c | 725 | ||||
-rw-r--r-- | src/libserver/http/http_message.h | 254 | ||||
-rw-r--r-- | src/libserver/http/http_private.h | 129 | ||||
-rw-r--r-- | src/libserver/http/http_router.c | 559 | ||||
-rw-r--r-- | src/libserver/http/http_router.h | 149 | ||||
-rw-r--r-- | src/libserver/http/http_util.c | 295 | ||||
-rw-r--r-- | src/libserver/http/http_util.h | 47 |
11 files changed, 5919 insertions, 0 deletions
diff --git a/src/libserver/http/http_connection.c b/src/libserver/http/http_connection.c new file mode 100644 index 0000000..5557fbf --- /dev/null +++ b/src/libserver/http/http_connection.c @@ -0,0 +1,2649 @@ +/*- + * Copyright 2016 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "config.h" +#include "http_connection.h" +#include "http_private.h" +#include "http_message.h" +#include "utlist.h" +#include "util.h" +#include "printf.h" +#include "logger.h" +#include "ref.h" +#include "ottery.h" +#include "keypair_private.h" +#include "cryptobox.h" +#include "libutil/libev_helper.h" +#include "libserver/ssl_util.h" +#include "libserver/url.h" + +#include "contrib/mumhash/mum.h" +#include "contrib/http-parser/http_parser.h" +#include "unix-std.h" + +#include <openssl/err.h> + +#define ENCRYPTED_VERSION " HTTP/1.0" + +struct _rspamd_http_privbuf { + rspamd_fstring_t *data; + const gchar *zc_buf; + gsize zc_remain; + ref_entry_t ref; +}; + +enum rspamd_http_priv_flags { + RSPAMD_HTTP_CONN_FLAG_ENCRYPTED = 1u << 0u, + RSPAMD_HTTP_CONN_FLAG_NEW_HEADER = 1u << 1u, + RSPAMD_HTTP_CONN_FLAG_RESETED = 1u << 2u, + RSPAMD_HTTP_CONN_FLAG_TOO_LARGE = 1u << 3u, + RSPAMD_HTTP_CONN_FLAG_ENCRYPTION_NEEDED = 1u << 4u, + RSPAMD_HTTP_CONN_FLAG_PROXY = 1u << 5u, + RSPAMD_HTTP_CONN_FLAG_PROXY_REQUEST = 1u << 6u, + RSPAMD_HTTP_CONN_OWN_SOCKET = 1u << 7u, +}; + +#define IS_CONN_ENCRYPTED(c) ((c)->flags & RSPAMD_HTTP_CONN_FLAG_ENCRYPTED) +#define IS_CONN_RESETED(c) ((c)->flags & RSPAMD_HTTP_CONN_FLAG_RESETED) + +struct rspamd_http_connection_private { + struct rspamd_http_context *ctx; + struct rspamd_ssl_connection *ssl; + struct _rspamd_http_privbuf *buf; + struct rspamd_keypair_cache *cache; + struct rspamd_cryptobox_pubkey *peer_key; + struct rspamd_cryptobox_keypair *local_key; + struct rspamd_http_header *header; + struct http_parser parser; + struct http_parser_settings parser_cb; + struct rspamd_io_ev ev; + ev_tstamp timeout; + struct rspamd_http_message *msg; + struct iovec *out; + guint outlen; + enum rspamd_http_priv_flags flags; + gsize wr_pos; + gsize wr_total; +}; + +static const rspamd_ftok_t key_header = { + .begin = "Key", + .len = 3}; +static const rspamd_ftok_t date_header = { + .begin = "Date", + .len = 4}; +static const rspamd_ftok_t last_modified_header = { + .begin = "Last-Modified", + .len = 13}; + +static void rspamd_http_event_handler(int fd, short what, gpointer ud); +static void rspamd_http_ssl_err_handler(gpointer ud, GError *err); + + +#define HTTP_ERROR http_error_quark() +GQuark +http_error_quark(void) +{ + return g_quark_from_static_string("http-error-quark"); +} + +static void +rspamd_http_privbuf_dtor(gpointer ud) +{ + struct _rspamd_http_privbuf *p = (struct _rspamd_http_privbuf *) ud; + + if (p->data) { + rspamd_fstring_free(p->data); + } + + g_free(p); +} + +static const gchar * +rspamd_http_code_to_str(gint code) +{ + if (code == 200) { + return "OK"; + } + else if (code == 404) { + return "Not found"; + } + else if (code == 403 || code == 401) { + return "Not authorized"; + } + else if (code >= 400 && code < 500) { + return "Bad request"; + } + else if (code >= 300 && code < 400) { + return "See Other"; + } + else if (code >= 500 && code < 600) { + return "Internal server error"; + } + + return "Unknown error"; +} + +static void +rspamd_http_parse_key(rspamd_ftok_t *data, struct rspamd_http_connection *conn, + struct rspamd_http_connection_private *priv) +{ + guchar *decoded_id; + const gchar *eq_pos; + gsize id_len; + struct rspamd_cryptobox_pubkey *pk; + + if (priv->local_key == NULL) { + /* In this case we cannot do anything, e.g. we cannot decrypt payload */ + priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_ENCRYPTED; + } + else { + /* Check sanity of what we have */ + eq_pos = memchr(data->begin, '=', data->len); + if (eq_pos != NULL) { + decoded_id = rspamd_decode_base32(data->begin, eq_pos - data->begin, + &id_len, RSPAMD_BASE32_DEFAULT); + + if (decoded_id != NULL && id_len >= RSPAMD_KEYPAIR_SHORT_ID_LEN) { + pk = rspamd_pubkey_from_base32(eq_pos + 1, + data->begin + data->len - eq_pos - 1, + RSPAMD_KEYPAIR_KEX, + RSPAMD_CRYPTOBOX_MODE_25519); + if (pk != NULL) { + if (memcmp(rspamd_keypair_get_id(priv->local_key), + decoded_id, + RSPAMD_KEYPAIR_SHORT_ID_LEN) == 0) { + priv->msg->peer_key = pk; + + if (priv->cache && priv->msg->peer_key) { + rspamd_keypair_cache_process(priv->cache, + priv->local_key, + priv->msg->peer_key); + } + } + else { + rspamd_pubkey_unref(pk); + } + } + } + + priv->flags |= RSPAMD_HTTP_CONN_FLAG_ENCRYPTED; + g_free(decoded_id); + } + } +} + +static inline void +rspamd_http_check_special_header(struct rspamd_http_connection *conn, + struct rspamd_http_connection_private *priv) +{ + if (rspamd_ftok_casecmp(&priv->header->name, &date_header) == 0) { + priv->msg->date = rspamd_http_parse_date(priv->header->value.begin, + priv->header->value.len); + } + else if (rspamd_ftok_casecmp(&priv->header->name, &key_header) == 0) { + rspamd_http_parse_key(&priv->header->value, conn, priv); + } + else if (rspamd_ftok_casecmp(&priv->header->name, &last_modified_header) == 0) { + priv->msg->last_modified = rspamd_http_parse_date( + priv->header->value.begin, + priv->header->value.len); + } +} + +static gint +rspamd_http_on_url(http_parser *parser, const gchar *at, size_t length) +{ + struct rspamd_http_connection *conn = + (struct rspamd_http_connection *) parser->data; + struct rspamd_http_connection_private *priv; + + priv = conn->priv; + + priv->msg->url = rspamd_fstring_append(priv->msg->url, at, length); + + return 0; +} + +static gint +rspamd_http_on_status(http_parser *parser, const gchar *at, size_t length) +{ + struct rspamd_http_connection *conn = + (struct rspamd_http_connection *) parser->data; + struct rspamd_http_connection_private *priv; + + priv = conn->priv; + + if (parser->status_code != 200) { + if (priv->msg->status == NULL) { + priv->msg->status = rspamd_fstring_new(); + } + + priv->msg->status = rspamd_fstring_append(priv->msg->status, at, length); + } + + return 0; +} + +static void +rspamd_http_finish_header(struct rspamd_http_connection *conn, + struct rspamd_http_connection_private *priv) +{ + struct rspamd_http_header *hdr; + khiter_t k; + gint r; + + priv->header->combined = rspamd_fstring_append(priv->header->combined, + "\r\n", 2); + priv->header->value.len = priv->header->combined->len - + priv->header->name.len - 4; + priv->header->value.begin = priv->header->combined->str + + priv->header->name.len + 2; + priv->header->name.begin = priv->header->combined->str; + + k = kh_put(rspamd_http_headers_hash, priv->msg->headers, &priv->header->name, + &r); + + if (r != 0) { + kh_value(priv->msg->headers, k) = priv->header; + hdr = NULL; + } + else { + hdr = kh_value(priv->msg->headers, k); + } + + DL_APPEND(hdr, priv->header); + + rspamd_http_check_special_header(conn, priv); +} + +static void +rspamd_http_init_header(struct rspamd_http_connection_private *priv) +{ + priv->header = g_malloc0(sizeof(struct rspamd_http_header)); + priv->header->combined = rspamd_fstring_new(); +} + +static gint +rspamd_http_on_header_field(http_parser *parser, + const gchar *at, + size_t length) +{ + struct rspamd_http_connection *conn = + (struct rspamd_http_connection *) parser->data; + struct rspamd_http_connection_private *priv; + + priv = conn->priv; + + if (priv->header == NULL) { + rspamd_http_init_header(priv); + } + else if (priv->flags & RSPAMD_HTTP_CONN_FLAG_NEW_HEADER) { + rspamd_http_finish_header(conn, priv); + rspamd_http_init_header(priv); + } + + priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_NEW_HEADER; + priv->header->combined = rspamd_fstring_append(priv->header->combined, + at, length); + + return 0; +} + +static gint +rspamd_http_on_header_value(http_parser *parser, + const gchar *at, + size_t length) +{ + struct rspamd_http_connection *conn = + (struct rspamd_http_connection *) parser->data; + struct rspamd_http_connection_private *priv; + + priv = conn->priv; + + if (priv->header == NULL) { + /* Should not happen */ + return -1; + } + + if (!(priv->flags & RSPAMD_HTTP_CONN_FLAG_NEW_HEADER)) { + priv->flags |= RSPAMD_HTTP_CONN_FLAG_NEW_HEADER; + priv->header->combined = rspamd_fstring_append(priv->header->combined, + ": ", 2); + priv->header->name.len = priv->header->combined->len - 2; + } + + priv->header->combined = rspamd_fstring_append(priv->header->combined, + at, length); + + return 0; +} + +static int +rspamd_http_on_headers_complete(http_parser *parser) +{ + struct rspamd_http_connection *conn = + (struct rspamd_http_connection *) parser->data; + struct rspamd_http_connection_private *priv; + struct rspamd_http_message *msg; + int ret; + + priv = conn->priv; + msg = priv->msg; + + if (priv->header != NULL) { + rspamd_http_finish_header(conn, priv); + + priv->header = NULL; + priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_NEW_HEADER; + } + + if (msg->method == HTTP_HEAD) { + /* We don't care about the rest */ + rspamd_ev_watcher_stop(priv->ctx->event_loop, &priv->ev); + + msg->code = parser->status_code; + rspamd_http_connection_ref(conn); + ret = conn->finish_handler(conn, msg); + + if (conn->opts & RSPAMD_HTTP_CLIENT_KEEP_ALIVE) { + rspamd_http_context_push_keepalive(conn->priv->ctx, conn, + msg, conn->priv->ctx->event_loop); + rspamd_http_connection_reset(conn); + } + else { + conn->finished = TRUE; + } + + rspamd_http_connection_unref(conn); + + return ret; + } + + /* + * HTTP parser sets content length to (-1) when it doesn't know the real + * length, for example, in case of chunked encoding. + * + * Hence, we skip body setup here + */ + if (parser->content_length != ULLONG_MAX && parser->content_length != 0 && + msg->method != HTTP_HEAD) { + if (conn->max_size > 0 && + parser->content_length > conn->max_size) { + /* Too large message */ + priv->flags |= RSPAMD_HTTP_CONN_FLAG_TOO_LARGE; + return -1; + } + + if (!rspamd_http_message_set_body(msg, NULL, parser->content_length)) { + return -1; + } + } + + if (parser->flags & F_SPAMC) { + msg->flags |= RSPAMD_HTTP_FLAG_SPAMC; + } + + + msg->method = parser->method; + msg->code = parser->status_code; + + return 0; +} + +static void +rspamd_http_switch_zc(struct _rspamd_http_privbuf *pbuf, + struct rspamd_http_message *msg) +{ + pbuf->zc_buf = msg->body_buf.begin + msg->body_buf.len; + pbuf->zc_remain = msg->body_buf.allocated_len - msg->body_buf.len; +} + +static int +rspamd_http_on_body(http_parser *parser, const gchar *at, size_t length) +{ + struct rspamd_http_connection *conn = + (struct rspamd_http_connection *) parser->data; + struct rspamd_http_connection_private *priv; + struct rspamd_http_message *msg; + struct _rspamd_http_privbuf *pbuf; + const gchar *p; + + priv = conn->priv; + msg = priv->msg; + pbuf = priv->buf; + p = at; + + if (!(msg->flags & RSPAMD_HTTP_FLAG_HAS_BODY)) { + if (!rspamd_http_message_set_body(msg, NULL, parser->content_length)) { + return -1; + } + } + + if (conn->finished) { + return 0; + } + + if (conn->max_size > 0 && + msg->body_buf.len + length > conn->max_size) { + /* Body length overflow */ + priv->flags |= RSPAMD_HTTP_CONN_FLAG_TOO_LARGE; + return -1; + } + + if (!pbuf->zc_buf) { + if (!rspamd_http_message_append_body(msg, at, length)) { + return -1; + } + + /* We might have some leftover in our private buffer */ + if (pbuf->data->len == length) { + /* Switch to zero-copy mode */ + rspamd_http_switch_zc(pbuf, msg); + } + } + else { + if (msg->body_buf.begin + msg->body_buf.len != at) { + /* Likely chunked encoding */ + memmove((gchar *) msg->body_buf.begin + msg->body_buf.len, at, length); + p = msg->body_buf.begin + msg->body_buf.len; + } + + /* Adjust zero-copy buf */ + msg->body_buf.len += length; + + if (!(msg->flags & RSPAMD_HTTP_FLAG_SHMEM)) { + msg->body_buf.c.normal->len += length; + } + + pbuf->zc_buf = msg->body_buf.begin + msg->body_buf.len; + pbuf->zc_remain = msg->body_buf.allocated_len - msg->body_buf.len; + } + + if ((conn->opts & RSPAMD_HTTP_BODY_PARTIAL) && !IS_CONN_ENCRYPTED(priv)) { + /* Incremental update is impossible for encrypted requests so far */ + return (conn->body_handler(conn, msg, p, length)); + } + + return 0; +} + +static int +rspamd_http_on_body_decrypted(http_parser *parser, const gchar *at, size_t length) +{ + struct rspamd_http_connection *conn = + (struct rspamd_http_connection *) parser->data; + struct rspamd_http_connection_private *priv; + + priv = conn->priv; + + if (priv->header != NULL) { + rspamd_http_finish_header(conn, priv); + priv->header = NULL; + } + + if (conn->finished) { + return 0; + } + + if (priv->msg->body_buf.len == 0) { + + priv->msg->body_buf.begin = at; + priv->msg->method = parser->method; + priv->msg->code = parser->status_code; + } + + priv->msg->body_buf.len += length; + + return 0; +} + +static int +rspamd_http_on_headers_complete_decrypted(http_parser *parser) +{ + struct rspamd_http_connection *conn = + (struct rspamd_http_connection *) parser->data; + struct rspamd_http_connection_private *priv; + struct rspamd_http_message *msg; + int ret; + + priv = conn->priv; + msg = priv->msg; + + if (priv->header != NULL) { + rspamd_http_finish_header(conn, priv); + + priv->header = NULL; + priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_NEW_HEADER; + } + + if (parser->flags & F_SPAMC) { + priv->msg->flags |= RSPAMD_HTTP_FLAG_SPAMC; + } + + if (msg->method == HTTP_HEAD) { + /* We don't care about the rest */ + rspamd_ev_watcher_stop(priv->ctx->event_loop, &priv->ev); + msg->code = parser->status_code; + rspamd_http_connection_ref(conn); + ret = conn->finish_handler(conn, msg); + + if (conn->opts & RSPAMD_HTTP_CLIENT_KEEP_ALIVE) { + rspamd_http_context_push_keepalive(conn->priv->ctx, conn, + msg, conn->priv->ctx->event_loop); + rspamd_http_connection_reset(conn); + } + else { + conn->finished = TRUE; + } + + rspamd_http_connection_unref(conn); + + return ret; + } + + priv->msg->method = parser->method; + priv->msg->code = parser->status_code; + + return 0; +} + +static int +rspamd_http_decrypt_message(struct rspamd_http_connection *conn, + struct rspamd_http_connection_private *priv, + struct rspamd_cryptobox_pubkey *peer_key) +{ + guchar *nonce, *m; + const guchar *nm; + gsize dec_len; + struct rspamd_http_message *msg = priv->msg; + struct rspamd_http_header *hdr, *hcur, *hcurtmp; + struct http_parser decrypted_parser; + struct http_parser_settings decrypted_cb; + enum rspamd_cryptobox_mode mode; + + mode = rspamd_keypair_alg(priv->local_key); + nonce = msg->body_buf.str; + m = msg->body_buf.str + rspamd_cryptobox_nonce_bytes(mode) + + rspamd_cryptobox_mac_bytes(mode); + dec_len = msg->body_buf.len - rspamd_cryptobox_nonce_bytes(mode) - + rspamd_cryptobox_mac_bytes(mode); + + if ((nm = rspamd_pubkey_get_nm(peer_key, priv->local_key)) == NULL) { + nm = rspamd_pubkey_calculate_nm(peer_key, priv->local_key); + } + + if (!rspamd_cryptobox_decrypt_nm_inplace(m, dec_len, nonce, + nm, m - rspamd_cryptobox_mac_bytes(mode), mode)) { + msg_err("cannot verify encrypted message, first bytes of the input: %*xs", + (gint) MIN(msg->body_buf.len, 64), msg->body_buf.begin); + return -1; + } + + /* Cleanup message */ + kh_foreach_value (msg->headers, hdr, { + DL_FOREACH_SAFE (hdr, hcur, hcurtmp) { + rspamd_fstring_free (hcur->combined); + g_free (hcur); +} +}); + +kh_destroy(rspamd_http_headers_hash, msg->headers); +msg->headers = kh_init(rspamd_http_headers_hash); + +if (msg->url != NULL) { + msg->url = rspamd_fstring_assign(msg->url, "", 0); +} + +msg->body_buf.len = 0; + +memset(&decrypted_parser, 0, sizeof(decrypted_parser)); +http_parser_init(&decrypted_parser, + conn->type == RSPAMD_HTTP_SERVER ? HTTP_REQUEST : HTTP_RESPONSE); + +memset(&decrypted_cb, 0, sizeof(decrypted_cb)); +decrypted_cb.on_url = rspamd_http_on_url; +decrypted_cb.on_status = rspamd_http_on_status; +decrypted_cb.on_header_field = rspamd_http_on_header_field; +decrypted_cb.on_header_value = rspamd_http_on_header_value; +decrypted_cb.on_headers_complete = rspamd_http_on_headers_complete_decrypted; +decrypted_cb.on_body = rspamd_http_on_body_decrypted; +decrypted_parser.data = conn; +decrypted_parser.content_length = dec_len; + +if (http_parser_execute(&decrypted_parser, &decrypted_cb, m, + dec_len) != (size_t) dec_len) { + msg_err("HTTP parser error: %s when parsing encrypted request", + http_errno_description(decrypted_parser.http_errno)); + return -1; +} + +return 0; +} + +static int +rspamd_http_on_message_complete(http_parser *parser) +{ + struct rspamd_http_connection *conn = + (struct rspamd_http_connection *) parser->data; + struct rspamd_http_connection_private *priv; + int ret = 0; + enum rspamd_cryptobox_mode mode; + + if (conn->finished) { + return 0; + } + + priv = conn->priv; + + if ((conn->opts & RSPAMD_HTTP_REQUIRE_ENCRYPTION) && !IS_CONN_ENCRYPTED(priv)) { + priv->flags |= RSPAMD_HTTP_CONN_FLAG_ENCRYPTION_NEEDED; + msg_err("unencrypted connection when encryption has been requested"); + return -1; + } + + if ((conn->opts & RSPAMD_HTTP_BODY_PARTIAL) == 0 && IS_CONN_ENCRYPTED(priv)) { + mode = rspamd_keypair_alg(priv->local_key); + + if (priv->local_key == NULL || priv->msg->peer_key == NULL || + priv->msg->body_buf.len < rspamd_cryptobox_nonce_bytes(mode) + + rspamd_cryptobox_mac_bytes(mode)) { + msg_err("cannot decrypt message"); + return -1; + } + + /* We have keys, so we can decrypt message */ + ret = rspamd_http_decrypt_message(conn, priv, priv->msg->peer_key); + + if (ret != 0) { + return ret; + } + + if (conn->body_handler != NULL) { + rspamd_http_connection_ref(conn); + ret = conn->body_handler(conn, + priv->msg, + priv->msg->body_buf.begin, + priv->msg->body_buf.len); + rspamd_http_connection_unref(conn); + } + } + else if ((conn->opts & RSPAMD_HTTP_BODY_PARTIAL) == 0 && conn->body_handler) { + g_assert(conn->body_handler != NULL); + rspamd_http_connection_ref(conn); + ret = conn->body_handler(conn, + priv->msg, + priv->msg->body_buf.begin, + priv->msg->body_buf.len); + rspamd_http_connection_unref(conn); + } + + if (ret == 0) { + rspamd_ev_watcher_stop(priv->ctx->event_loop, &priv->ev); + rspamd_http_connection_ref(conn); + ret = conn->finish_handler(conn, priv->msg); + + if (conn->opts & RSPAMD_HTTP_CLIENT_KEEP_ALIVE) { + rspamd_http_context_push_keepalive(conn->priv->ctx, conn, + priv->msg, conn->priv->ctx->event_loop); + rspamd_http_connection_reset(conn); + } + else { + conn->finished = TRUE; + } + + rspamd_http_connection_unref(conn); + } + + return ret; +} + +static void +rspamd_http_simple_client_helper(struct rspamd_http_connection *conn) +{ + struct rspamd_http_connection_private *priv; + gpointer ssl; + gint request_method; + GString *prev_host = NULL; + + priv = conn->priv; + ssl = priv->ssl; + priv->ssl = NULL; + + /* Preserve data */ + if (priv->msg) { + request_method = priv->msg->method; + /* Preserve host for keepalive */ + prev_host = priv->msg->host; + priv->msg->host = NULL; + } + + rspamd_http_connection_reset(conn); + priv->ssl = ssl; + + /* Plan read message */ + + if (conn->opts & RSPAMD_HTTP_CLIENT_SHARED) { + rspamd_http_connection_read_message_shared(conn, conn->ud, + conn->priv->timeout); + } + else { + rspamd_http_connection_read_message(conn, conn->ud, + conn->priv->timeout); + } + + if (priv->msg) { + priv->msg->method = request_method; + priv->msg->host = prev_host; + } + else { + if (prev_host) { + g_string_free(prev_host, TRUE); + } + } +} + +static void +rspamd_http_write_helper(struct rspamd_http_connection *conn) +{ + struct rspamd_http_connection_private *priv; + struct iovec *start; + guint niov, i; + gint flags = 0; + gsize remain; + gssize r; + GError *err; + struct iovec *cur_iov; + struct msghdr msg; + + priv = conn->priv; + + if (priv->wr_pos == priv->wr_total) { + goto call_finish_handler; + } + + start = &priv->out[0]; + niov = priv->outlen; + remain = priv->wr_pos; + /* We know that niov is small enough for that */ + if (priv->ssl) { + /* Might be recursive! */ + cur_iov = g_malloc(niov * sizeof(struct iovec)); + } + else { + cur_iov = alloca(niov * sizeof(struct iovec)); + } + memcpy(cur_iov, priv->out, niov * sizeof(struct iovec)); + for (i = 0; i < priv->outlen && remain > 0; i++) { + /* Find out the first iov required */ + start = &cur_iov[i]; + if (start->iov_len <= remain) { + remain -= start->iov_len; + start = &cur_iov[i + 1]; + niov--; + } + else { + start->iov_base = (void *) ((char *) start->iov_base + remain); + start->iov_len -= remain; + remain = 0; + } + } + + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = start; + msg.msg_iovlen = MIN(IOV_MAX, niov); + g_assert(niov > 0); +#ifdef MSG_NOSIGNAL + flags = MSG_NOSIGNAL; +#endif + + if (priv->ssl) { + r = rspamd_ssl_writev(priv->ssl, msg.msg_iov, msg.msg_iovlen); + g_free(cur_iov); + } + else { + r = sendmsg(conn->fd, &msg, flags); + } + + if (r == -1) { + if (!priv->ssl) { + err = g_error_new(HTTP_ERROR, 500, "IO write error: %s", strerror(errno)); + rspamd_http_connection_ref(conn); + conn->error_handler(conn, err); + rspamd_http_connection_unref(conn); + g_error_free(err); + } + + return; + } + else { + priv->wr_pos += r; + } + + if (priv->wr_pos >= priv->wr_total) { + goto call_finish_handler; + } + else { + /* Want to write more */ + priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_RESETED; + + if (priv->ssl && r > 0) { + /* We can write more data... */ + rspamd_http_write_helper(conn); + return; + } + } + + return; + +call_finish_handler: + rspamd_ev_watcher_stop(priv->ctx->event_loop, &priv->ev); + + if ((conn->opts & RSPAMD_HTTP_CLIENT_SIMPLE) == 0) { + rspamd_http_connection_ref(conn); + conn->finished = TRUE; + conn->finish_handler(conn, priv->msg); + rspamd_http_connection_unref(conn); + } + else { + /* Plan read message */ + rspamd_http_simple_client_helper(conn); + } +} + +static gssize +rspamd_http_try_read(gint fd, + struct rspamd_http_connection *conn, + struct rspamd_http_connection_private *priv, + struct _rspamd_http_privbuf *pbuf, + const gchar **buf_ptr) +{ + gssize r; + gchar *data; + gsize len; + struct rspamd_http_message *msg; + + msg = priv->msg; + + if (pbuf->zc_buf == NULL) { + data = priv->buf->data->str; + len = priv->buf->data->allocated; + } + else { + data = (gchar *) pbuf->zc_buf; + len = pbuf->zc_remain; + + if (len == 0) { + rspamd_http_message_grow_body(priv->msg, priv->buf->data->allocated); + rspamd_http_switch_zc(pbuf, msg); + data = (gchar *) pbuf->zc_buf; + len = pbuf->zc_remain; + } + } + + if (priv->ssl) { + r = rspamd_ssl_read(priv->ssl, data, len); + } + else { + r = read(fd, data, len); + } + + if (r <= 0) { + return r; + } + else { + if (pbuf->zc_buf == NULL) { + priv->buf->data->len = r; + } + else { + pbuf->zc_remain -= r; + pbuf->zc_buf += r; + } + } + + if (buf_ptr) { + *buf_ptr = data; + } + + return r; +} + +static void +rspamd_http_ssl_err_handler(gpointer ud, GError *err) +{ + struct rspamd_http_connection *conn = (struct rspamd_http_connection *) ud; + + rspamd_http_connection_ref(conn); + conn->error_handler(conn, err); + rspamd_http_connection_unref(conn); +} + +static void +rspamd_http_event_handler(int fd, short what, gpointer ud) +{ + struct rspamd_http_connection *conn = (struct rspamd_http_connection *) ud; + struct rspamd_http_connection_private *priv; + struct _rspamd_http_privbuf *pbuf; + const gchar *d; + gssize r; + GError *err; + + priv = conn->priv; + pbuf = priv->buf; + REF_RETAIN(pbuf); + rspamd_http_connection_ref(conn); + + if (what == EV_READ) { + r = rspamd_http_try_read(fd, conn, priv, pbuf, &d); + + if (r > 0) { + if (http_parser_execute(&priv->parser, &priv->parser_cb, + d, r) != (size_t) r || + priv->parser.http_errno != 0) { + if (priv->flags & RSPAMD_HTTP_CONN_FLAG_TOO_LARGE) { + err = g_error_new(HTTP_ERROR, 413, + "Request entity too large: %zu", + (size_t) priv->parser.content_length); + } + else if (priv->flags & RSPAMD_HTTP_CONN_FLAG_ENCRYPTION_NEEDED) { + err = g_error_new(HTTP_ERROR, 400, + "Encryption required"); + } + else if (priv->parser.http_errno == HPE_CLOSED_CONNECTION) { + msg_err("got garbage after end of the message, ignore it"); + + REF_RELEASE(pbuf); + rspamd_http_connection_unref(conn); + + return; + } + else { + if (priv->parser.http_errno > HPE_CB_status) { + err = g_error_new(HTTP_ERROR, 400, + "HTTP parser error: %s", + http_errno_description(priv->parser.http_errno)); + } + else { + err = g_error_new(HTTP_ERROR, 500, + "HTTP parser internal error: %s", + http_errno_description(priv->parser.http_errno)); + } + } + + if (!conn->finished) { + conn->error_handler(conn, err); + } + else { + msg_err("got error after HTTP request is finished: %e", err); + } + + g_error_free(err); + + REF_RELEASE(pbuf); + rspamd_http_connection_unref(conn); + + return; + } + } + else if (r == 0) { + /* We can still call http parser */ + http_parser_execute(&priv->parser, &priv->parser_cb, d, r); + + if (!conn->finished) { + err = g_error_new(HTTP_ERROR, + 400, + "IO read error: unexpected EOF"); + conn->error_handler(conn, err); + g_error_free(err); + } + REF_RELEASE(pbuf); + rspamd_http_connection_unref(conn); + + return; + } + else { + if (!priv->ssl) { + err = g_error_new(HTTP_ERROR, + 500, + "HTTP IO read error: %s", + strerror(errno)); + conn->error_handler(conn, err); + g_error_free(err); + } + + REF_RELEASE(pbuf); + rspamd_http_connection_unref(conn); + + return; + } + } + else if (what == EV_TIMEOUT) { + if (!priv->ssl) { + /* Let's try to read from the socket first */ + r = rspamd_http_try_read(fd, conn, priv, pbuf, &d); + + if (r > 0) { + if (http_parser_execute(&priv->parser, &priv->parser_cb, + d, r) != (size_t) r || + priv->parser.http_errno != 0) { + err = g_error_new(HTTP_ERROR, 400, + "HTTP parser error: %s", + http_errno_description(priv->parser.http_errno)); + + if (!conn->finished) { + conn->error_handler(conn, err); + } + else { + msg_err("got error after HTTP request is finished: %e", err); + } + + g_error_free(err); + + REF_RELEASE(pbuf); + rspamd_http_connection_unref(conn); + + return; + } + } + else { + err = g_error_new(HTTP_ERROR, 408, + "IO timeout"); + conn->error_handler(conn, err); + g_error_free(err); + + REF_RELEASE(pbuf); + rspamd_http_connection_unref(conn); + + return; + } + } + else { + /* In case of SSL we disable this logic as we already came from SSL handler */ + REF_RELEASE(pbuf); + rspamd_http_connection_unref(conn); + + return; + } + } + else if (what == EV_WRITE) { + rspamd_http_write_helper(conn); + } + + REF_RELEASE(pbuf); + rspamd_http_connection_unref(conn); +} + +static void +rspamd_http_parser_reset(struct rspamd_http_connection *conn) +{ + struct rspamd_http_connection_private *priv = conn->priv; + + http_parser_init(&priv->parser, + conn->type == RSPAMD_HTTP_SERVER ? HTTP_REQUEST : HTTP_RESPONSE); + + priv->parser_cb.on_url = rspamd_http_on_url; + priv->parser_cb.on_status = rspamd_http_on_status; + priv->parser_cb.on_header_field = rspamd_http_on_header_field; + priv->parser_cb.on_header_value = rspamd_http_on_header_value; + priv->parser_cb.on_headers_complete = rspamd_http_on_headers_complete; + priv->parser_cb.on_body = rspamd_http_on_body; + priv->parser_cb.on_message_complete = rspamd_http_on_message_complete; +} + +static struct rspamd_http_connection * +rspamd_http_connection_new_common(struct rspamd_http_context *ctx, + gint fd, + rspamd_http_body_handler_t body_handler, + rspamd_http_error_handler_t error_handler, + rspamd_http_finish_handler_t finish_handler, + unsigned opts, + enum rspamd_http_connection_type type, + enum rspamd_http_priv_flags priv_flags, + struct upstream *proxy_upstream) +{ + struct rspamd_http_connection *conn; + struct rspamd_http_connection_private *priv; + + g_assert(error_handler != NULL && finish_handler != NULL); + + if (ctx == NULL) { + ctx = rspamd_http_context_default(); + } + + conn = g_malloc0(sizeof(struct rspamd_http_connection)); + conn->opts = opts; + conn->type = type; + conn->body_handler = body_handler; + conn->error_handler = error_handler; + conn->finish_handler = finish_handler; + conn->fd = fd; + conn->ref = 1; + conn->finished = FALSE; + + /* Init priv */ + priv = g_malloc0(sizeof(struct rspamd_http_connection_private)); + conn->priv = priv; + priv->ctx = ctx; + priv->flags = priv_flags; + + if (type == RSPAMD_HTTP_SERVER) { + priv->cache = ctx->server_kp_cache; + } + else { + priv->cache = ctx->client_kp_cache; + if (ctx->client_kp) { + priv->local_key = rspamd_keypair_ref(ctx->client_kp); + } + } + + rspamd_http_parser_reset(conn); + priv->parser.data = conn; + + return conn; +} + +struct rspamd_http_connection * +rspamd_http_connection_new_server(struct rspamd_http_context *ctx, + gint fd, + rspamd_http_body_handler_t body_handler, + rspamd_http_error_handler_t error_handler, + rspamd_http_finish_handler_t finish_handler, + unsigned opts) +{ + return rspamd_http_connection_new_common(ctx, fd, body_handler, + error_handler, finish_handler, opts, RSPAMD_HTTP_SERVER, 0, NULL); +} + +struct rspamd_http_connection * +rspamd_http_connection_new_client_socket(struct rspamd_http_context *ctx, + rspamd_http_body_handler_t body_handler, + rspamd_http_error_handler_t error_handler, + rspamd_http_finish_handler_t finish_handler, + unsigned opts, + gint fd) +{ + return rspamd_http_connection_new_common(ctx, fd, body_handler, + error_handler, finish_handler, opts, RSPAMD_HTTP_CLIENT, 0, NULL); +} + +struct rspamd_http_connection * +rspamd_http_connection_new_client(struct rspamd_http_context *ctx, + rspamd_http_body_handler_t body_handler, + rspamd_http_error_handler_t error_handler, + rspamd_http_finish_handler_t finish_handler, + unsigned opts, + rspamd_inet_addr_t *addr) +{ + gint fd; + + if (ctx == NULL) { + ctx = rspamd_http_context_default(); + } + + if (ctx->http_proxies) { + struct upstream *up = rspamd_upstream_get(ctx->http_proxies, + RSPAMD_UPSTREAM_ROUND_ROBIN, NULL, 0); + + if (up) { + rspamd_inet_addr_t *proxy_addr = rspamd_upstream_addr_next(up); + + fd = rspamd_inet_address_connect(proxy_addr, SOCK_STREAM, TRUE); + + if (fd == -1) { + msg_info("cannot connect to http proxy %s: %s", + rspamd_inet_address_to_string_pretty(proxy_addr), + strerror(errno)); + rspamd_upstream_fail(up, TRUE, strerror(errno)); + + return NULL; + } + + return rspamd_http_connection_new_common(ctx, fd, body_handler, + error_handler, finish_handler, opts, + RSPAMD_HTTP_CLIENT, + RSPAMD_HTTP_CONN_OWN_SOCKET | RSPAMD_HTTP_CONN_FLAG_PROXY, + up); + } + } + + /* Unproxied version */ + fd = rspamd_inet_address_connect(addr, SOCK_STREAM, TRUE); + + if (fd == -1) { + msg_info("cannot connect make http connection to %s: %s", + rspamd_inet_address_to_string_pretty(addr), + strerror(errno)); + + return NULL; + } + + return rspamd_http_connection_new_common(ctx, fd, body_handler, + error_handler, finish_handler, opts, + RSPAMD_HTTP_CLIENT, + RSPAMD_HTTP_CONN_OWN_SOCKET, + NULL); +} + +struct rspamd_http_connection * +rspamd_http_connection_new_client_keepalive(struct rspamd_http_context *ctx, + rspamd_http_body_handler_t body_handler, + rspamd_http_error_handler_t error_handler, + rspamd_http_finish_handler_t finish_handler, + unsigned opts, + rspamd_inet_addr_t *addr, + const gchar *host) +{ + struct rspamd_http_connection *conn; + + if (ctx == NULL) { + ctx = rspamd_http_context_default(); + } + + conn = rspamd_http_context_check_keepalive(ctx, addr, host, + opts & RSPAMD_HTTP_CLIENT_SSL); + + if (conn) { + return conn; + } + + conn = rspamd_http_connection_new_client(ctx, + body_handler, error_handler, finish_handler, + opts | RSPAMD_HTTP_CLIENT_SIMPLE | RSPAMD_HTTP_CLIENT_KEEP_ALIVE, + addr); + + if (conn) { + rspamd_http_context_prepare_keepalive(ctx, conn, addr, host, + opts & RSPAMD_HTTP_CLIENT_SSL); + } + + return conn; +} + +void rspamd_http_connection_reset(struct rspamd_http_connection *conn) +{ + struct rspamd_http_connection_private *priv; + struct rspamd_http_message *msg; + + priv = conn->priv; + msg = priv->msg; + + /* Clear request */ + if (msg != NULL) { + if (msg->peer_key) { + priv->peer_key = msg->peer_key; + msg->peer_key = NULL; + } + rspamd_http_message_unref(msg); + priv->msg = NULL; + } + + conn->finished = FALSE; + /* Clear priv */ + rspamd_ev_watcher_stop(priv->ctx->event_loop, &priv->ev); + + if (!(priv->flags & RSPAMD_HTTP_CONN_FLAG_RESETED)) { + rspamd_http_parser_reset(conn); + } + + if (priv->buf != NULL) { + REF_RELEASE(priv->buf); + priv->buf = NULL; + } + + if (priv->out != NULL) { + g_free(priv->out); + priv->out = NULL; + } + + priv->flags |= RSPAMD_HTTP_CONN_FLAG_RESETED; +} + +struct rspamd_http_message * +rspamd_http_connection_steal_msg(struct rspamd_http_connection *conn) +{ + struct rspamd_http_connection_private *priv; + struct rspamd_http_message *msg; + + priv = conn->priv; + msg = priv->msg; + + /* Clear request */ + if (msg != NULL) { + if (msg->peer_key) { + priv->peer_key = msg->peer_key; + msg->peer_key = NULL; + } + priv->msg = NULL; + } + + return msg; +} + +struct rspamd_http_message * +rspamd_http_connection_copy_msg(struct rspamd_http_message *msg, GError **err) +{ + struct rspamd_http_message *new_msg; + struct rspamd_http_header *hdr, *nhdr, *nhdrs, *hcur; + const gchar *old_body; + gsize old_len; + struct stat st; + union _rspamd_storage_u *storage; + + new_msg = rspamd_http_new_message(msg->type); + new_msg->flags = msg->flags; + + if (msg->body_buf.len > 0) { + + if (msg->flags & RSPAMD_HTTP_FLAG_SHMEM) { + /* Avoid copying by just mapping a shared segment */ + new_msg->flags |= RSPAMD_HTTP_FLAG_SHMEM_IMMUTABLE; + + storage = &new_msg->body_buf.c; + storage->shared.shm_fd = dup(msg->body_buf.c.shared.shm_fd); + + if (storage->shared.shm_fd == -1) { + rspamd_http_message_unref(new_msg); + g_set_error(err, http_error_quark(), errno, + "cannot dup shmem fd: %d: %s", + msg->body_buf.c.shared.shm_fd, strerror(errno)); + + return NULL; + } + + if (fstat(storage->shared.shm_fd, &st) == -1) { + g_set_error(err, http_error_quark(), errno, + "cannot stat shmem fd: %d: %s", + storage->shared.shm_fd, strerror(errno)); + rspamd_http_message_unref(new_msg); + + return NULL; + } + + /* We don't own segment, so do not try to touch it */ + + if (msg->body_buf.c.shared.name) { + storage->shared.name = msg->body_buf.c.shared.name; + REF_RETAIN(storage->shared.name); + } + + new_msg->body_buf.str = mmap(NULL, st.st_size, + PROT_READ, MAP_SHARED, + storage->shared.shm_fd, 0); + + if (new_msg->body_buf.str == MAP_FAILED) { + g_set_error(err, http_error_quark(), errno, + "cannot mmap shmem fd: %d: %s", + storage->shared.shm_fd, strerror(errno)); + rspamd_http_message_unref(new_msg); + + return NULL; + } + + new_msg->body_buf.begin = new_msg->body_buf.str; + new_msg->body_buf.len = msg->body_buf.len; + new_msg->body_buf.begin = new_msg->body_buf.str + + (msg->body_buf.begin - msg->body_buf.str); + } + else { + old_body = rspamd_http_message_get_body(msg, &old_len); + + if (!rspamd_http_message_set_body(new_msg, old_body, old_len)) { + g_set_error(err, http_error_quark(), errno, + "cannot set body for message, length: %zd", + old_len); + rspamd_http_message_unref(new_msg); + + return NULL; + } + } + } + + if (msg->url) { + if (new_msg->url) { + new_msg->url = rspamd_fstring_append(new_msg->url, msg->url->str, + msg->url->len); + } + else { + new_msg->url = rspamd_fstring_new_init(msg->url->str, + msg->url->len); + } + } + + if (msg->host) { + new_msg->host = g_string_new_len(msg->host->str, msg->host->len); + } + + new_msg->method = msg->method; + new_msg->port = msg->port; + new_msg->date = msg->date; + new_msg->last_modified = msg->last_modified; + + kh_foreach_value(msg->headers, hdr, { + nhdrs = NULL; + + DL_FOREACH(hdr, hcur) + { + nhdr = g_malloc(sizeof(struct rspamd_http_header)); + + nhdr->combined = rspamd_fstring_new_init(hcur->combined->str, + hcur->combined->len); + nhdr->name.begin = nhdr->combined->str + + (hcur->name.begin - hcur->combined->str); + nhdr->name.len = hcur->name.len; + nhdr->value.begin = nhdr->combined->str + + (hcur->value.begin - hcur->combined->str); + nhdr->value.len = hcur->value.len; + DL_APPEND(nhdrs, nhdr); + } + + gint r; + khiter_t k = kh_put(rspamd_http_headers_hash, new_msg->headers, + &nhdrs->name, &r); + + if (r != 0) { + kh_value(new_msg->headers, k) = nhdrs; + } + else { + DL_CONCAT(kh_value(new_msg->headers, k), nhdrs); + } + }); + + return new_msg; +} + +void rspamd_http_connection_free(struct rspamd_http_connection *conn) +{ + struct rspamd_http_connection_private *priv; + + priv = conn->priv; + + if (priv != NULL) { + rspamd_http_connection_reset(conn); + + if (priv->ssl) { + rspamd_ssl_connection_free(priv->ssl); + priv->ssl = NULL; + } + + if (priv->local_key) { + rspamd_keypair_unref(priv->local_key); + } + if (priv->peer_key) { + rspamd_pubkey_unref(priv->peer_key); + } + + if (priv->flags & RSPAMD_HTTP_CONN_OWN_SOCKET) { + /* Fd is owned by a connection */ + close(conn->fd); + } + + g_free(priv); + } + + g_free(conn); +} + +static void +rspamd_http_connection_read_message_common(struct rspamd_http_connection *conn, + gpointer ud, ev_tstamp timeout, + gint flags) +{ + struct rspamd_http_connection_private *priv = conn->priv; + struct rspamd_http_message *req; + + conn->ud = ud; + req = rspamd_http_new_message( + conn->type == RSPAMD_HTTP_SERVER ? HTTP_REQUEST : HTTP_RESPONSE); + priv->msg = req; + req->flags = flags; + + if (flags & RSPAMD_HTTP_FLAG_SHMEM) { + req->body_buf.c.shared.shm_fd = -1; + } + + if (priv->peer_key) { + priv->msg->peer_key = priv->peer_key; + priv->peer_key = NULL; + priv->flags |= RSPAMD_HTTP_CONN_FLAG_ENCRYPTED; + } + + priv->timeout = timeout; + priv->header = NULL; + priv->buf = g_malloc0(sizeof(*priv->buf)); + REF_INIT_RETAIN(priv->buf, rspamd_http_privbuf_dtor); + priv->buf->data = rspamd_fstring_sized_new(8192); + priv->flags |= RSPAMD_HTTP_CONN_FLAG_NEW_HEADER; + + if (!priv->ssl) { + rspamd_ev_watcher_init(&priv->ev, conn->fd, EV_READ, + rspamd_http_event_handler, conn); + rspamd_ev_watcher_start(priv->ctx->event_loop, &priv->ev, priv->timeout); + } + else { + rspamd_ssl_connection_restore_handlers(priv->ssl, + rspamd_http_event_handler, + rspamd_http_ssl_err_handler, + conn, + EV_READ); + } + + priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_RESETED; +} + +void rspamd_http_connection_read_message(struct rspamd_http_connection *conn, + gpointer ud, ev_tstamp timeout) +{ + rspamd_http_connection_read_message_common(conn, ud, timeout, 0); +} + +void rspamd_http_connection_read_message_shared(struct rspamd_http_connection *conn, + gpointer ud, ev_tstamp timeout) +{ + rspamd_http_connection_read_message_common(conn, ud, timeout, + RSPAMD_HTTP_FLAG_SHMEM); +} + +static void +rspamd_http_connection_encrypt_message( + struct rspamd_http_connection *conn, + struct rspamd_http_message *msg, + struct rspamd_http_connection_private *priv, + guchar *pbody, + guint bodylen, + guchar *pmethod, + guint methodlen, + guint preludelen, + gint hdrcount, + guchar *np, + guchar *mp, + struct rspamd_cryptobox_pubkey *peer_key) +{ + struct rspamd_cryptobox_segment *segments; + guchar *crlfp; + const guchar *nm; + gint i, cnt; + guint outlen; + struct rspamd_http_header *hdr, *hcur; + enum rspamd_cryptobox_mode mode; + + mode = rspamd_keypair_alg(priv->local_key); + crlfp = mp + rspamd_cryptobox_mac_bytes(mode); + + outlen = priv->out[0].iov_len + priv->out[1].iov_len; + /* + * Create segments from the following: + * Method, [URL], CRLF, nheaders, CRLF, body + */ + segments = g_new(struct rspamd_cryptobox_segment, hdrcount + 5); + + segments[0].data = pmethod; + segments[0].len = methodlen; + + if (conn->type != RSPAMD_HTTP_SERVER) { + segments[1].data = msg->url->str; + segments[1].len = msg->url->len; + /* space + HTTP version + crlf */ + segments[2].data = crlfp; + segments[2].len = preludelen - 2; + crlfp += segments[2].len; + i = 3; + } + else { + /* Here we send just CRLF */ + segments[1].data = crlfp; + segments[1].len = 2; + crlfp += segments[1].len; + + i = 2; + } + + + kh_foreach_value (msg->headers, hdr, { + DL_FOREACH (hdr, hcur) { + segments[i].data = hcur->combined->str; + segments[i++].len = hcur->combined->len; +} +}); + +/* crlfp should point now at the second crlf */ +segments[i].data = crlfp; +segments[i++].len = 2; + +if (pbody) { + segments[i].data = pbody; + segments[i++].len = bodylen; +} + +cnt = i; + +if ((nm = rspamd_pubkey_get_nm(peer_key, priv->local_key)) == NULL) { + nm = rspamd_pubkey_calculate_nm(peer_key, priv->local_key); +} + +rspamd_cryptobox_encryptv_nm_inplace(segments, cnt, np, nm, mp, mode); + +/* + * iov[0] = base HTTP request + * iov[1] = CRLF + * iov[2] = nonce + * iov[3] = mac + * iov[4..i] = encrypted HTTP request/reply + */ +priv->out[2].iov_base = np; +priv->out[2].iov_len = rspamd_cryptobox_nonce_bytes(mode); +priv->out[3].iov_base = mp; +priv->out[3].iov_len = rspamd_cryptobox_mac_bytes(mode); + +outlen += rspamd_cryptobox_nonce_bytes(mode) + + rspamd_cryptobox_mac_bytes(mode); + +for (i = 0; i < cnt; i++) { + priv->out[i + 4].iov_base = segments[i].data; + priv->out[i + 4].iov_len = segments[i].len; + outlen += segments[i].len; +} + +priv->wr_total = outlen; + +g_free(segments); +} + +static void +rspamd_http_detach_shared(struct rspamd_http_message *msg) +{ + rspamd_fstring_t *cpy_str; + + cpy_str = rspamd_fstring_new_init(msg->body_buf.begin, msg->body_buf.len); + rspamd_http_message_set_body_from_fstring_steal(msg, cpy_str); +} + +gint rspamd_http_message_write_header(const gchar *mime_type, gboolean encrypted, + gchar *repbuf, gsize replen, gsize bodylen, gsize enclen, const gchar *host, + struct rspamd_http_connection *conn, struct rspamd_http_message *msg, + rspamd_fstring_t **buf, + struct rspamd_http_connection_private *priv, + struct rspamd_cryptobox_pubkey *peer_key) +{ + gchar datebuf[64]; + gint meth_len = 0; + const gchar *conn_type = "close"; + + if (conn->type == RSPAMD_HTTP_SERVER) { + /* Format reply */ + if (msg->method < HTTP_SYMBOLS) { + rspamd_ftok_t status; + + rspamd_http_date_format(datebuf, sizeof(datebuf), msg->date); + + if (mime_type == NULL) { + mime_type = + encrypted ? "application/octet-stream" : "text/plain"; + } + + if (msg->status == NULL || msg->status->len == 0) { + if (msg->code == 200) { + RSPAMD_FTOK_ASSIGN(&status, "OK"); + } + else if (msg->code == 404) { + RSPAMD_FTOK_ASSIGN(&status, "Not Found"); + } + else if (msg->code == 403) { + RSPAMD_FTOK_ASSIGN(&status, "Forbidden"); + } + else if (msg->code >= 500 && msg->code < 600) { + RSPAMD_FTOK_ASSIGN(&status, "Internal Server Error"); + } + else { + RSPAMD_FTOK_ASSIGN(&status, "Undefined Error"); + } + } + else { + status.begin = msg->status->str; + status.len = msg->status->len; + } + + if (encrypted) { + /* Internal reply (encrypted) */ + if (mime_type) { + meth_len = + rspamd_snprintf(repbuf, replen, + "HTTP/1.1 %d %T\r\n" + "Connection: close\r\n" + "Server: %s\r\n" + "Date: %s\r\n" + "Content-Length: %z\r\n" + "Content-Type: %s", /* NO \r\n at the end ! */ + msg->code, &status, priv->ctx->config.server_hdr, + datebuf, + bodylen, mime_type); + } + else { + meth_len = + rspamd_snprintf(repbuf, replen, + "HTTP/1.1 %d %T\r\n" + "Connection: close\r\n" + "Server: %s\r\n" + "Date: %s\r\n" + "Content-Length: %z", /* NO \r\n at the end ! */ + msg->code, &status, priv->ctx->config.server_hdr, + datebuf, + bodylen); + } + enclen += meth_len; + /* External reply */ + rspamd_printf_fstring(buf, + "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Server: %s\r\n" + "Date: %s\r\n" + "Content-Length: %z\r\n" + "Content-Type: application/octet-stream\r\n", + priv->ctx->config.server_hdr, + datebuf, enclen); + } + else { + if (mime_type) { + meth_len = + rspamd_printf_fstring(buf, + "HTTP/1.1 %d %T\r\n" + "Connection: close\r\n" + "Server: %s\r\n" + "Date: %s\r\n" + "Content-Length: %z\r\n" + "Content-Type: %s\r\n", + msg->code, &status, priv->ctx->config.server_hdr, + datebuf, + bodylen, mime_type); + } + else { + meth_len = + rspamd_printf_fstring(buf, + "HTTP/1.1 %d %T\r\n" + "Connection: close\r\n" + "Server: %s\r\n" + "Date: %s\r\n" + "Content-Length: %z\r\n", + msg->code, &status, priv->ctx->config.server_hdr, + datebuf, + bodylen); + } + } + } + else { + /* Legacy spamd reply */ + if (msg->flags & RSPAMD_HTTP_FLAG_SPAMC) { + gsize real_bodylen; + goffset eoh_pos; + GString tmp; + + /* Unfortunately, spamc protocol is deadly brain damaged */ + tmp.str = (gchar *) msg->body_buf.begin; + tmp.len = msg->body_buf.len; + + if (rspamd_string_find_eoh(&tmp, &eoh_pos) != -1 && + bodylen > eoh_pos) { + real_bodylen = bodylen - eoh_pos; + } + else { + real_bodylen = bodylen; + } + + rspamd_printf_fstring(buf, "SPAMD/1.1 0 EX_OK\r\n" + "Content-length: %z\r\n", + real_bodylen); + } + else { + rspamd_printf_fstring(buf, "RSPAMD/1.3 0 EX_OK\r\n"); + } + } + } + else { + + /* Client request */ + if (conn->opts & RSPAMD_HTTP_CLIENT_KEEP_ALIVE) { + conn_type = "keep-alive"; + } + + /* Format request */ + enclen += RSPAMD_FSTRING_LEN(msg->url) + + strlen(http_method_str(msg->method)) + 1; + + if (host == NULL && msg->host == NULL) { + /* Fallback to HTTP/1.0 */ + if (encrypted) { + rspamd_printf_fstring(buf, + "%s %s HTTP/1.0\r\n" + "Content-Length: %z\r\n" + "Content-Type: application/octet-stream\r\n" + "Connection: %s\r\n", + "POST", + "/post", + enclen, + conn_type); + } + else { + rspamd_printf_fstring(buf, + "%s %V HTTP/1.0\r\n" + "Content-Length: %z\r\n" + "Connection: %s\r\n", + http_method_str(msg->method), + msg->url, + bodylen, + conn_type); + + if (bodylen > 0) { + if (mime_type == NULL) { + mime_type = "text/plain"; + } + + rspamd_printf_fstring(buf, + "Content-Type: %s\r\n", + mime_type); + } + } + } + else { + /* Normal HTTP/1.1 with Host */ + if (host == NULL) { + host = msg->host->str; + } + + if (encrypted) { + /* TODO: Add proxy support to HTTPCrypt */ + if (rspamd_http_message_is_standard_port(msg)) { + rspamd_printf_fstring(buf, + "%s %s HTTP/1.1\r\n" + "Connection: %s\r\n" + "Host: %s\r\n" + "Content-Length: %z\r\n" + "Content-Type: application/octet-stream\r\n", + "POST", + "/post", + conn_type, + host, + enclen); + } + else { + rspamd_printf_fstring(buf, + "%s %s HTTP/1.1\r\n" + "Connection: %s\r\n" + "Host: %s:%d\r\n" + "Content-Length: %z\r\n" + "Content-Type: application/octet-stream\r\n", + "POST", + "/post", + conn_type, + host, + msg->port, + enclen); + } + } + else { + if (conn->priv->flags & RSPAMD_HTTP_CONN_FLAG_PROXY) { + /* Write proxied request */ + if ((msg->flags & RSPAMD_HTTP_FLAG_HAS_HOST_HEADER)) { + rspamd_printf_fstring(buf, + "%s %s://%s:%d/%V HTTP/1.1\r\n" + "Connection: %s\r\n" + "Content-Length: %z\r\n", + http_method_str(msg->method), + (conn->opts & RSPAMD_HTTP_CLIENT_SSL) ? "https" : "http", + host, + msg->port, + msg->url, + conn_type, + bodylen); + } + else { + if (rspamd_http_message_is_standard_port(msg)) { + rspamd_printf_fstring(buf, + "%s %s://%s:%d/%V HTTP/1.1\r\n" + "Connection: %s\r\n" + "Host: %s\r\n" + "Content-Length: %z\r\n", + http_method_str(msg->method), + (conn->opts & RSPAMD_HTTP_CLIENT_SSL) ? "https" : "http", + host, + msg->port, + msg->url, + conn_type, + host, + bodylen); + } + else { + rspamd_printf_fstring(buf, + "%s %s://%s:%d/%V HTTP/1.1\r\n" + "Connection: %s\r\n" + "Host: %s:%d\r\n" + "Content-Length: %z\r\n", + http_method_str(msg->method), + (conn->opts & RSPAMD_HTTP_CLIENT_SSL) ? "https" : "http", + host, + msg->port, + msg->url, + conn_type, + host, + msg->port, + bodylen); + } + } + } + else { + /* Unproxied version */ + if ((msg->flags & RSPAMD_HTTP_FLAG_HAS_HOST_HEADER)) { + rspamd_printf_fstring(buf, + "%s %V HTTP/1.1\r\n" + "Connection: %s\r\n" + "Content-Length: %z\r\n", + http_method_str(msg->method), + msg->url, + conn_type, + bodylen); + } + else { + if (rspamd_http_message_is_standard_port(msg)) { + rspamd_printf_fstring(buf, + "%s %V HTTP/1.1\r\n" + "Connection: %s\r\n" + "Host: %s\r\n" + "Content-Length: %z\r\n", + http_method_str(msg->method), + msg->url, + conn_type, + host, + bodylen); + } + else { + rspamd_printf_fstring(buf, + "%s %V HTTP/1.1\r\n" + "Connection: %s\r\n" + "Host: %s:%d\r\n" + "Content-Length: %z\r\n", + http_method_str(msg->method), + msg->url, + conn_type, + host, + msg->port, + bodylen); + } + } + } + + if (bodylen > 0) { + if (mime_type != NULL) { + rspamd_printf_fstring(buf, + "Content-Type: %s\r\n", + mime_type); + } + } + } + } + + if (encrypted) { + GString *b32_key, *b32_id; + + b32_key = rspamd_keypair_print(priv->local_key, + RSPAMD_KEYPAIR_PUBKEY | RSPAMD_KEYPAIR_BASE32); + b32_id = rspamd_pubkey_print(peer_key, + RSPAMD_KEYPAIR_ID_SHORT | RSPAMD_KEYPAIR_BASE32); + /* XXX: add some fuzz here */ + rspamd_printf_fstring(&*buf, "Key: %v=%v\r\n", b32_id, b32_key); + g_string_free(b32_key, TRUE); + g_string_free(b32_id, TRUE); + } + } + + return meth_len; +} + +static gboolean +rspamd_http_connection_write_message_common(struct rspamd_http_connection *conn, + struct rspamd_http_message *msg, + const gchar *host, + const gchar *mime_type, + gpointer ud, + ev_tstamp timeout, + gboolean allow_shared) +{ + struct rspamd_http_connection_private *priv = conn->priv; + struct rspamd_http_header *hdr, *hcur; + gchar repbuf[512], *pbody; + gint i, hdrcount, meth_len = 0, preludelen = 0; + gsize bodylen, enclen = 0; + rspamd_fstring_t *buf; + gboolean encrypted = FALSE; + guchar nonce[rspamd_cryptobox_MAX_NONCEBYTES], mac[rspamd_cryptobox_MAX_MACBYTES]; + guchar *np = NULL, *mp = NULL, *meth_pos = NULL; + struct rspamd_cryptobox_pubkey *peer_key = NULL; + enum rspamd_cryptobox_mode mode; + GError *err; + + conn->ud = ud; + priv->msg = msg; + priv->timeout = timeout; + + priv->header = NULL; + priv->buf = g_malloc0(sizeof(*priv->buf)); + REF_INIT_RETAIN(priv->buf, rspamd_http_privbuf_dtor); + priv->buf->data = rspamd_fstring_sized_new(512); + buf = priv->buf->data; + + if ((msg->flags & RSPAMD_HTTP_FLAG_WANT_SSL) && !(conn->opts & RSPAMD_HTTP_CLIENT_SSL)) { + err = g_error_new(HTTP_ERROR, 400, + "SSL connection requested but not created properly, internal error"); + rspamd_http_connection_ref(conn); + conn->error_handler(conn, err); + rspamd_http_connection_unref(conn); + g_error_free(err); + return FALSE; + } + + if (priv->peer_key && priv->local_key) { + priv->msg->peer_key = priv->peer_key; + priv->peer_key = NULL; + priv->flags |= RSPAMD_HTTP_CONN_FLAG_ENCRYPTED; + } + + if (msg->peer_key != NULL) { + if (priv->local_key == NULL) { + /* Automatically generate a temporary keypair */ + priv->local_key = rspamd_keypair_new(RSPAMD_KEYPAIR_KEX, + RSPAMD_CRYPTOBOX_MODE_25519); + } + + encrypted = TRUE; + + if (priv->cache) { + rspamd_keypair_cache_process(priv->cache, + priv->local_key, priv->msg->peer_key); + } + } + + if (encrypted && (msg->flags & + (RSPAMD_HTTP_FLAG_SHMEM_IMMUTABLE | RSPAMD_HTTP_FLAG_SHMEM))) { + /* We cannot use immutable body to encrypt message in place */ + allow_shared = FALSE; + rspamd_http_detach_shared(msg); + } + + if (allow_shared) { + gchar tmpbuf[64]; + + if (!(msg->flags & RSPAMD_HTTP_FLAG_SHMEM) || + msg->body_buf.c.shared.name == NULL) { + allow_shared = FALSE; + } + else { + /* Insert new headers */ + rspamd_http_message_add_header(msg, "Shm", + msg->body_buf.c.shared.name->shm_name); + rspamd_snprintf(tmpbuf, sizeof(tmpbuf), "%d", + (int) (msg->body_buf.begin - msg->body_buf.str)); + rspamd_http_message_add_header(msg, "Shm-Offset", + tmpbuf); + rspamd_snprintf(tmpbuf, sizeof(tmpbuf), "%z", + msg->body_buf.len); + rspamd_http_message_add_header(msg, "Shm-Length", + tmpbuf); + } + } + + if (priv->ctx->config.user_agent && conn->type == RSPAMD_HTTP_CLIENT) { + rspamd_ftok_t srch; + khiter_t k; + gint r; + + RSPAMD_FTOK_ASSIGN(&srch, "User-Agent"); + + k = kh_put(rspamd_http_headers_hash, msg->headers, &srch, &r); + + if (r != 0) { + hdr = g_malloc0(sizeof(struct rspamd_http_header)); + guint vlen = strlen(priv->ctx->config.user_agent); + hdr->combined = rspamd_fstring_sized_new(srch.len + vlen + 4); + rspamd_printf_fstring(&hdr->combined, "%T: %*s\r\n", &srch, vlen, + priv->ctx->config.user_agent); + hdr->name.begin = hdr->combined->str; + hdr->name.len = srch.len; + hdr->value.begin = hdr->combined->str + srch.len + 2; + hdr->value.len = vlen; + hdr->prev = hdr; /* for utlists */ + + kh_value(msg->headers, k) = hdr; + /* as we searched using static buffer */ + kh_key(msg->headers, k) = &hdr->name; + } + } + + if (encrypted) { + mode = rspamd_keypair_alg(priv->local_key); + + if (msg->body_buf.len == 0) { + pbody = NULL; + bodylen = 0; + msg->method = HTTP_GET; + } + else { + pbody = (gchar *) msg->body_buf.begin; + bodylen = msg->body_buf.len; + msg->method = HTTP_POST; + } + + if (conn->type == RSPAMD_HTTP_SERVER) { + /* + * iov[0] = base reply + * iov[1] = CRLF + * iov[2] = nonce + * iov[3] = mac + * iov[4] = encrypted reply + * iov[6] = encrypted crlf + * iov[7..n] = encrypted headers + * iov[n + 1] = encrypted crlf + * [iov[n + 2] = encrypted body] + */ + priv->outlen = 7; + enclen = rspamd_cryptobox_nonce_bytes(mode) + + rspamd_cryptobox_mac_bytes(mode) + + 4 + /* 2 * CRLF */ + bodylen; + } + else { + /* + * iov[0] = base request + * iov[1] = CRLF + * iov[2] = nonce + * iov[3] = mac + * iov[4] = encrypted method + space + * iov[5] = encrypted url + * iov[7] = encrypted prelude + * iov[8..n] = encrypted headers + * iov[n + 1] = encrypted crlf + * [iov[n + 2] = encrypted body] + */ + priv->outlen = 8; + + if (bodylen > 0) { + if (mime_type != NULL) { + preludelen = rspamd_snprintf(repbuf, sizeof(repbuf), "%s\r\n" + "Content-Length: %z\r\n" + "Content-Type: %s\r\n" + "\r\n", + ENCRYPTED_VERSION, bodylen, + mime_type); + } + else { + preludelen = rspamd_snprintf(repbuf, sizeof(repbuf), "%s\r\n" + "Content-Length: %z\r\n" + "" + "\r\n", + ENCRYPTED_VERSION, bodylen); + } + } + else { + preludelen = rspamd_snprintf(repbuf, sizeof(repbuf), + "%s\r\n\r\n", + ENCRYPTED_VERSION); + } + + enclen = rspamd_cryptobox_nonce_bytes(mode) + + rspamd_cryptobox_mac_bytes(mode) + + preludelen + /* version [content-length] + 2 * CRLF */ + bodylen; + } + + if (bodylen > 0) { + priv->outlen++; + } + } + else { + if (msg->method < HTTP_SYMBOLS) { + if (msg->body_buf.len == 0 || allow_shared) { + pbody = NULL; + bodylen = 0; + priv->outlen = 2; + + if (msg->method == HTTP_INVALID) { + msg->method = HTTP_GET; + } + } + else { + pbody = (gchar *) msg->body_buf.begin; + bodylen = msg->body_buf.len; + priv->outlen = 3; + + if (msg->method == HTTP_INVALID) { + msg->method = HTTP_POST; + } + } + } + else if (msg->body_buf.len > 0) { + allow_shared = FALSE; + pbody = (gchar *) msg->body_buf.begin; + bodylen = msg->body_buf.len; + priv->outlen = 2; + } + else { + /* Invalid body for spamc method */ + abort(); + } + } + + peer_key = msg->peer_key; + + priv->wr_total = bodylen + 2; + + hdrcount = 0; + + if (msg->method < HTTP_SYMBOLS) { + kh_foreach_value (msg->headers, hdr, { + DL_FOREACH (hdr, hcur) { + /* <name: value\r\n> */ + priv->wr_total += hcur->combined->len; + enclen += hcur->combined->len; + priv->outlen ++; + hdrcount ++; + } +}); +} + +/* Allocate iov */ +priv->out = g_malloc0(sizeof(struct iovec) * priv->outlen); +priv->wr_pos = 0; + +meth_len = rspamd_http_message_write_header(mime_type, encrypted, + repbuf, sizeof(repbuf), bodylen, enclen, + host, conn, msg, + &buf, priv, peer_key); +priv->wr_total += buf->len; + +/* Setup external request body */ +priv->out[0].iov_base = buf->str; +priv->out[0].iov_len = buf->len; + +/* Buf will be used eventually for encryption */ +if (encrypted) { + gint meth_offset, nonce_offset, mac_offset; + mode = rspamd_keypair_alg(priv->local_key); + + ottery_rand_bytes(nonce, rspamd_cryptobox_nonce_bytes(mode)); + memset(mac, 0, rspamd_cryptobox_mac_bytes(mode)); + meth_offset = buf->len; + + if (conn->type == RSPAMD_HTTP_SERVER) { + buf = rspamd_fstring_append(buf, repbuf, meth_len); + } + else { + meth_len = strlen(http_method_str(msg->method)) + 1; /* + space */ + buf = rspamd_fstring_append(buf, http_method_str(msg->method), + meth_len - 1); + buf = rspamd_fstring_append(buf, " ", 1); + } + + nonce_offset = buf->len; + buf = rspamd_fstring_append(buf, nonce, + rspamd_cryptobox_nonce_bytes(mode)); + mac_offset = buf->len; + buf = rspamd_fstring_append(buf, mac, + rspamd_cryptobox_mac_bytes(mode)); + + /* Need to be encrypted */ + if (conn->type == RSPAMD_HTTP_SERVER) { + buf = rspamd_fstring_append(buf, "\r\n\r\n", 4); + } + else { + buf = rspamd_fstring_append(buf, repbuf, preludelen); + } + + meth_pos = buf->str + meth_offset; + np = buf->str + nonce_offset; + mp = buf->str + mac_offset; +} + +/* During previous writes, buf might be reallocated and changed */ +priv->buf->data = buf; + +if (encrypted) { + /* Finish external HTTP request */ + priv->out[1].iov_base = "\r\n"; + priv->out[1].iov_len = 2; + /* Encrypt the real request */ + rspamd_http_connection_encrypt_message(conn, msg, priv, pbody, bodylen, + meth_pos, meth_len, preludelen, hdrcount, np, mp, peer_key); +} +else { + i = 1; + if (msg->method < HTTP_SYMBOLS) { + kh_foreach_value (msg->headers, hdr, { + DL_FOREACH (hdr, hcur) { + priv->out[i].iov_base = hcur->combined->str; + priv->out[i++].iov_len = hcur->combined->len; + } +}); + +priv->out[i].iov_base = "\r\n"; +priv->out[i++].iov_len = 2; +} +else +{ + /* No CRLF for compatibility reply */ + priv->wr_total -= 2; +} + +if (pbody != NULL) { + priv->out[i].iov_base = pbody; + priv->out[i++].iov_len = bodylen; +} +} + +priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_RESETED; + +if ((priv->flags & RSPAMD_HTTP_CONN_FLAG_PROXY) && (conn->opts & RSPAMD_HTTP_CLIENT_SSL)) { + /* We need to disable SSL flag! */ + err = g_error_new(HTTP_ERROR, 400, "cannot use proxy for SSL connections"); + rspamd_http_connection_ref(conn); + conn->error_handler(conn, err); + rspamd_http_connection_unref(conn); + g_error_free(err); + return FALSE; +} + +rspamd_ev_watcher_stop(priv->ctx->event_loop, &priv->ev); + +if (conn->opts & RSPAMD_HTTP_CLIENT_SSL) { + gpointer ssl_ctx = (msg->flags & RSPAMD_HTTP_FLAG_SSL_NOVERIFY) ? priv->ctx->ssl_ctx_noverify : priv->ctx->ssl_ctx; + + if (!ssl_ctx) { + err = g_error_new(HTTP_ERROR, 400, "ssl message requested " + "with no ssl ctx"); + rspamd_http_connection_ref(conn); + conn->error_handler(conn, err); + rspamd_http_connection_unref(conn); + g_error_free(err); + return FALSE; + } + else { + if (!priv->ssl) { + priv->ssl = rspamd_ssl_connection_new(ssl_ctx, priv->ctx->event_loop, + !(msg->flags & RSPAMD_HTTP_FLAG_SSL_NOVERIFY), + conn->log_tag); + g_assert(priv->ssl != NULL); + + if (!rspamd_ssl_connect_fd(priv->ssl, conn->fd, host, &priv->ev, + priv->timeout, rspamd_http_event_handler, + rspamd_http_ssl_err_handler, conn)) { + + err = g_error_new(HTTP_ERROR, 400, + "ssl connection error: ssl error=%s, errno=%s", + ERR_error_string(ERR_get_error(), NULL), + strerror(errno)); + rspamd_http_connection_ref(conn); + conn->error_handler(conn, err); + rspamd_http_connection_unref(conn); + g_error_free(err); + return FALSE; + } + } + else { + /* Just restore SSL handlers */ + rspamd_ssl_connection_restore_handlers(priv->ssl, + rspamd_http_event_handler, + rspamd_http_ssl_err_handler, + conn, + EV_WRITE); + } + } +} +else { + rspamd_ev_watcher_init(&priv->ev, conn->fd, EV_WRITE, + rspamd_http_event_handler, conn); + rspamd_ev_watcher_start(priv->ctx->event_loop, &priv->ev, priv->timeout); +} + +return TRUE; +} + +gboolean +rspamd_http_connection_write_message(struct rspamd_http_connection *conn, + struct rspamd_http_message *msg, + const gchar *host, + const gchar *mime_type, + gpointer ud, + ev_tstamp timeout) +{ + return rspamd_http_connection_write_message_common(conn, msg, host, mime_type, + ud, timeout, FALSE); +} + +gboolean +rspamd_http_connection_write_message_shared(struct rspamd_http_connection *conn, + struct rspamd_http_message *msg, + const gchar *host, + const gchar *mime_type, + gpointer ud, + ev_tstamp timeout) +{ + return rspamd_http_connection_write_message_common(conn, msg, host, mime_type, + ud, timeout, TRUE); +} + + +void rspamd_http_connection_set_max_size(struct rspamd_http_connection *conn, + gsize sz) +{ + conn->max_size = sz; +} + +void rspamd_http_connection_set_key(struct rspamd_http_connection *conn, + struct rspamd_cryptobox_keypair *key) +{ + struct rspamd_http_connection_private *priv = conn->priv; + + g_assert(key != NULL); + priv->local_key = rspamd_keypair_ref(key); +} + +void rspamd_http_connection_own_socket(struct rspamd_http_connection *conn) +{ + struct rspamd_http_connection_private *priv = conn->priv; + + priv->flags |= RSPAMD_HTTP_CONN_OWN_SOCKET; +} + +const struct rspamd_cryptobox_pubkey * +rspamd_http_connection_get_peer_key(struct rspamd_http_connection *conn) +{ + struct rspamd_http_connection_private *priv = conn->priv; + + if (priv->peer_key) { + return priv->peer_key; + } + else if (priv->msg) { + return priv->msg->peer_key; + } + + return NULL; +} + +gboolean +rspamd_http_connection_is_encrypted(struct rspamd_http_connection *conn) +{ + struct rspamd_http_connection_private *priv = conn->priv; + + if (priv->peer_key != NULL) { + return TRUE; + } + else if (priv->msg) { + return priv->msg->peer_key != NULL; + } + + return FALSE; +} + +GHashTable * +rspamd_http_message_parse_query(struct rspamd_http_message *msg) +{ + GHashTable *res; + rspamd_fstring_t *key = NULL, *value = NULL; + rspamd_ftok_t *key_tok = NULL, *value_tok = NULL; + const gchar *p, *c, *end; + struct http_parser_url u; + enum { + parse_key, + parse_eqsign, + parse_value, + parse_ampersand + } state = parse_key; + + res = g_hash_table_new_full(rspamd_ftok_icase_hash, + rspamd_ftok_icase_equal, + rspamd_fstring_mapped_ftok_free, + rspamd_fstring_mapped_ftok_free); + + if (msg->url && msg->url->len > 0) { + http_parser_parse_url(msg->url->str, msg->url->len, TRUE, &u); + + if (u.field_set & (1 << UF_QUERY)) { + p = msg->url->str + u.field_data[UF_QUERY].off; + c = p; + end = p + u.field_data[UF_QUERY].len; + + while (p <= end) { + switch (state) { + case parse_key: + if ((p == end || *p == '&') && p > c) { + /* We have a single parameter without a value */ + key = rspamd_fstring_new_init(c, p - c); + key_tok = rspamd_ftok_map(key); + key_tok->len = rspamd_url_decode(key->str, key->str, + key->len); + + value = rspamd_fstring_new_init("", 0); + value_tok = rspamd_ftok_map(value); + + g_hash_table_replace(res, key_tok, value_tok); + state = parse_ampersand; + } + else if (*p == '=' && p > c) { + /* We have something like key=value */ + key = rspamd_fstring_new_init(c, p - c); + key_tok = rspamd_ftok_map(key); + key_tok->len = rspamd_url_decode(key->str, key->str, + key->len); + + state = parse_eqsign; + } + else { + p++; + } + break; + + case parse_eqsign: + if (*p != '=') { + c = p; + state = parse_value; + } + else { + p++; + } + break; + + case parse_value: + if ((p == end || *p == '&') && p >= c) { + g_assert(key != NULL); + if (p > c) { + value = rspamd_fstring_new_init(c, p - c); + value_tok = rspamd_ftok_map(value); + value_tok->len = rspamd_url_decode(value->str, + value->str, + value->len); + /* Detect quotes for value */ + if (value_tok->begin[0] == '"') { + memmove(value->str, value->str + 1, + value_tok->len - 1); + value_tok->len--; + } + if (value_tok->begin[value_tok->len - 1] == '"') { + value_tok->len--; + } + } + else { + value = rspamd_fstring_new_init("", 0); + value_tok = rspamd_ftok_map(value); + } + + g_hash_table_replace(res, key_tok, value_tok); + key = value = NULL; + key_tok = value_tok = NULL; + state = parse_ampersand; + } + else { + p++; + } + break; + + case parse_ampersand: + if (p != end && *p != '&') { + c = p; + state = parse_key; + } + else { + p++; + } + break; + } + } + } + + if (state != parse_ampersand && key != NULL) { + rspamd_fstring_free(key); + } + } + + return res; +} + + +struct rspamd_http_message * +rspamd_http_message_ref(struct rspamd_http_message *msg) +{ + REF_RETAIN(msg); + + return msg; +} + +void rspamd_http_message_unref(struct rspamd_http_message *msg) +{ + REF_RELEASE(msg); +} + +void rspamd_http_connection_disable_encryption(struct rspamd_http_connection *conn) +{ + struct rspamd_http_connection_private *priv; + + priv = conn->priv; + + if (priv) { + if (priv->local_key) { + rspamd_keypair_unref(priv->local_key); + } + if (priv->peer_key) { + rspamd_pubkey_unref(priv->peer_key); + } + + priv->local_key = NULL; + priv->peer_key = NULL; + priv->flags &= ~RSPAMD_HTTP_CONN_FLAG_ENCRYPTED; + } +}
\ No newline at end of file diff --git a/src/libserver/http/http_connection.h b/src/libserver/http/http_connection.h new file mode 100644 index 0000000..e98d164 --- /dev/null +++ b/src/libserver/http/http_connection.h @@ -0,0 +1,320 @@ +/*- + * Copyright 2016 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef HTTP_H_ +#define HTTP_H_ + +/** + * @file http.h + * + * This is an interface for HTTP client and conn. + * This code uses HTTP parser written by Joyent Inc based on nginx code. + */ + +#include "config.h" +#include "http_context.h" +#include "fstring.h" +#include "ref.h" +#include "http_message.h" +#include "http_util.h" +#include "addr.h" + +#include "contrib/libev/ev.h" + +#ifdef __cplusplus +extern "C" { +#endif + +enum rspamd_http_connection_type { + RSPAMD_HTTP_SERVER, + RSPAMD_HTTP_CLIENT +}; + +struct rspamd_http_header; +struct rspamd_http_message; +struct rspamd_http_connection_private; +struct rspamd_http_connection; +struct rspamd_http_connection_router; +struct rspamd_http_connection_entry; +struct rspamd_keepalive_hash_key; + +struct rspamd_storage_shmem { + gchar *shm_name; + ref_entry_t ref; +}; + +/** + * Legacy spamc protocol + */ +#define RSPAMD_HTTP_FLAG_SPAMC (1 << 0) +/** + * Store body of the message in a shared memory segment + */ +#define RSPAMD_HTTP_FLAG_SHMEM (1 << 2) +/** + * Store body of the message in an immutable shared memory segment + */ +#define RSPAMD_HTTP_FLAG_SHMEM_IMMUTABLE (1 << 3) +/** + * Body has been set for a message + */ +#define RSPAMD_HTTP_FLAG_HAS_BODY (1 << 5) +/** + * Do not verify server's certificate + */ +#define RSPAMD_HTTP_FLAG_SSL_NOVERIFY (1 << 6) +/** + * Body has been set for a message + */ +#define RSPAMD_HTTP_FLAG_HAS_HOST_HEADER (1 << 7) +/** + * Message is intended for SSL connection + */ +#define RSPAMD_HTTP_FLAG_WANT_SSL (1 << 8) +/** + * Options for HTTP connection + */ +enum rspamd_http_options { + RSPAMD_HTTP_BODY_PARTIAL = 1, /**< Call body handler on all body data portions */ + RSPAMD_HTTP_CLIENT_SIMPLE = 1u << 1, /**< Read HTTP client reply automatically */ + RSPAMD_HTTP_CLIENT_ENCRYPTED = 1u << 2, /**< Encrypt data for client */ + RSPAMD_HTTP_CLIENT_SHARED = 1u << 3, /**< Store reply in shared memory */ + RSPAMD_HTTP_REQUIRE_ENCRYPTION = 1u << 4, + RSPAMD_HTTP_CLIENT_KEEP_ALIVE = 1u << 5, + RSPAMD_HTTP_CLIENT_SSL = 1u << 6u, +}; + +typedef int (*rspamd_http_body_handler_t)(struct rspamd_http_connection *conn, + struct rspamd_http_message *msg, + const gchar *chunk, + gsize len); + +typedef void (*rspamd_http_error_handler_t)(struct rspamd_http_connection *conn, + GError *err); + +typedef int (*rspamd_http_finish_handler_t)(struct rspamd_http_connection *conn, + struct rspamd_http_message *msg); + +/** + * HTTP connection structure + */ +struct rspamd_http_connection { + struct rspamd_http_connection_private *priv; + rspamd_http_body_handler_t body_handler; + rspamd_http_error_handler_t error_handler; + rspamd_http_finish_handler_t finish_handler; + gpointer ud; + const gchar *log_tag; + /* Used for keepalive */ + struct rspamd_keepalive_hash_key *keepalive_hash_key; + gsize max_size; + unsigned opts; + enum rspamd_http_connection_type type; + gboolean finished; + gint fd; + gint ref; +}; + +/** + * Creates a new HTTP server connection from an opened FD returned by accept function + * @param ctx + * @param fd + * @param body_handler + * @param error_handler + * @param finish_handler + * @param opts + * @return + */ +struct rspamd_http_connection *rspamd_http_connection_new_server( + struct rspamd_http_context *ctx, + gint fd, + rspamd_http_body_handler_t body_handler, + rspamd_http_error_handler_t error_handler, + rspamd_http_finish_handler_t finish_handler, + unsigned opts); + +/** + * Creates or reuses a new keepalive client connection identified by hostname and inet_addr + * @param ctx + * @param body_handler + * @param error_handler + * @param finish_handler + * @param addr + * @param host + * @return + */ +struct rspamd_http_connection *rspamd_http_connection_new_client_keepalive( + struct rspamd_http_context *ctx, + rspamd_http_body_handler_t body_handler, + rspamd_http_error_handler_t error_handler, + rspamd_http_finish_handler_t finish_handler, + unsigned opts, + rspamd_inet_addr_t *addr, + const gchar *host); + +/** + * Creates an ordinary connection using the address specified (if proxy is not set) + * @param ctx + * @param body_handler + * @param error_handler + * @param finish_handler + * @param opts + * @param addr + * @return + */ +struct rspamd_http_connection *rspamd_http_connection_new_client( + struct rspamd_http_context *ctx, + rspamd_http_body_handler_t body_handler, + rspamd_http_error_handler_t error_handler, + rspamd_http_finish_handler_t finish_handler, + unsigned opts, + rspamd_inet_addr_t *addr); + +/** + * Creates an ordinary client connection using ready file descriptor (ignores proxy) + * @param ctx + * @param body_handler + * @param error_handler + * @param finish_handler + * @param opts + * @param addr + * @return + */ +struct rspamd_http_connection *rspamd_http_connection_new_client_socket( + struct rspamd_http_context *ctx, + rspamd_http_body_handler_t body_handler, + rspamd_http_error_handler_t error_handler, + rspamd_http_finish_handler_t finish_handler, + unsigned opts, + gint fd); + +/** + * Set key pointed by an opaque pointer + * @param conn connection structure + * @param key opaque key structure + */ +void rspamd_http_connection_set_key(struct rspamd_http_connection *conn, + struct rspamd_cryptobox_keypair *key); + +/** + * Transfer ownership on socket to an HTTP connection + * @param conn + */ +void rspamd_http_connection_own_socket(struct rspamd_http_connection *conn); + +/** + * Get peer's public key + * @param conn connection structure + * @return pubkey structure or NULL + */ +const struct rspamd_cryptobox_pubkey *rspamd_http_connection_get_peer_key( + struct rspamd_http_connection *conn); + +/** + * Returns TRUE if a connection is encrypted + * @param conn + * @return + */ +gboolean rspamd_http_connection_is_encrypted(struct rspamd_http_connection *conn); + +/** + * Handle a request using socket fd and user data ud + * @param conn connection structure + * @param ud opaque user data + * @param fd fd to read/write + */ +void rspamd_http_connection_read_message( + struct rspamd_http_connection *conn, + gpointer ud, + ev_tstamp timeout); + +void rspamd_http_connection_read_message_shared( + struct rspamd_http_connection *conn, + gpointer ud, + ev_tstamp timeout); + +/** + * Send reply using initialised connection + * @param conn connection structure + * @param msg HTTP message + * @param ud opaque user data + * @param fd fd to read/write + */ +gboolean rspamd_http_connection_write_message( + struct rspamd_http_connection *conn, + struct rspamd_http_message *msg, + const gchar *host, + const gchar *mime_type, + gpointer ud, + ev_tstamp timeout); + +gboolean rspamd_http_connection_write_message_shared( + struct rspamd_http_connection *conn, + struct rspamd_http_message *msg, + const gchar *host, + const gchar *mime_type, + gpointer ud, + ev_tstamp timeout); + +/** + * Free connection structure + * @param conn + */ +void rspamd_http_connection_free(struct rspamd_http_connection *conn); + +/** + * Increase refcount for a connection + * @param conn + * @return + */ +static inline struct rspamd_http_connection * +rspamd_http_connection_ref(struct rspamd_http_connection *conn) +{ + conn->ref++; + return conn; +} + +/** + * Decrease a refcount for a connection and free it if refcount is equal to zero + * @param conn + */ +static void +rspamd_http_connection_unref(struct rspamd_http_connection *conn) +{ + if (--conn->ref <= 0) { + rspamd_http_connection_free(conn); + } +} + +/** + * Reset connection for a new request + * @param conn + */ +void rspamd_http_connection_reset(struct rspamd_http_connection *conn); + +/** + * Sets global maximum size for HTTP message being processed + * @param sz + */ +void rspamd_http_connection_set_max_size(struct rspamd_http_connection *conn, + gsize sz); + +void rspamd_http_connection_disable_encryption(struct rspamd_http_connection *conn); + +#ifdef __cplusplus +} +#endif + +#endif /* HTTP_H_ */ diff --git a/src/libserver/http/http_context.c b/src/libserver/http/http_context.c new file mode 100644 index 0000000..f08e33b --- /dev/null +++ b/src/libserver/http/http_context.c @@ -0,0 +1,670 @@ +/* + * Copyright 2023 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "http_context.h" +#include "http_private.h" +#include "keypair.h" +#include "keypairs_cache.h" +#include "cfg_file.h" +#include "contrib/libottery/ottery.h" +#include "contrib/http-parser/http_parser.h" +#include "ssl_util.h" +#include "rspamd.h" +#include "libev_helper.h" + +INIT_LOG_MODULE(http_context) + +#define msg_debug_http_context(...) rspamd_conditional_debug_fast(NULL, NULL, \ + rspamd_http_context_log_id, "http_context", NULL, \ + G_STRFUNC, \ + __VA_ARGS__) + +static struct rspamd_http_context *default_ctx = NULL; + +struct rspamd_http_keepalive_cbdata { + struct rspamd_http_connection *conn; + struct rspamd_http_context *ctx; + GQueue *queue; + GList *link; + struct rspamd_io_ev ev; +}; + +static void +rspamd_http_keepalive_queue_cleanup(GQueue *conns) +{ + GList *cur; + + cur = conns->head; + + while (cur) { + struct rspamd_http_keepalive_cbdata *cbd; + + cbd = (struct rspamd_http_keepalive_cbdata *) cur->data; + /* unref call closes fd, so we need to remove ev watcher first! */ + rspamd_ev_watcher_stop(cbd->ctx->event_loop, &cbd->ev); + rspamd_http_connection_unref(cbd->conn); + g_free(cbd); + + cur = cur->next; + } + + g_queue_clear(conns); +} + +static void +rspamd_http_context_client_rotate_ev(struct ev_loop *loop, ev_timer *w, int revents) +{ + struct rspamd_http_context *ctx = (struct rspamd_http_context *) w->data; + gpointer kp; + + w->repeat = rspamd_time_jitter(ctx->config.client_key_rotate_time, 0); + msg_debug_http_context("rotate local keypair, next rotate in %.0f seconds", + w->repeat); + + ev_timer_again(loop, w); + + kp = ctx->client_kp; + ctx->client_kp = rspamd_keypair_new(RSPAMD_KEYPAIR_KEX, + RSPAMD_CRYPTOBOX_MODE_25519); + rspamd_keypair_unref(kp); +} + +static struct rspamd_http_context * +rspamd_http_context_new_default(struct rspamd_config *cfg, + struct ev_loop *ev_base, + struct upstream_ctx *ups_ctx) +{ + struct rspamd_http_context *ctx; + + static const int default_kp_size = 1024; + static const gdouble default_rotate_time = 120; + static const gdouble default_keepalive_interval = 65; + static const gchar *default_user_agent = "rspamd-" RSPAMD_VERSION_FULL; + static const gchar *default_server_hdr = "rspamd/" RSPAMD_VERSION_FULL; + + ctx = g_malloc0(sizeof(*ctx)); + ctx->config.kp_cache_size_client = default_kp_size; + ctx->config.kp_cache_size_server = default_kp_size; + ctx->config.client_key_rotate_time = default_rotate_time; + ctx->config.user_agent = default_user_agent; + ctx->config.keepalive_interval = default_keepalive_interval; + ctx->config.server_hdr = default_server_hdr; + ctx->ups_ctx = ups_ctx; + + if (cfg) { + ctx->ssl_ctx = cfg->libs_ctx->ssl_ctx; + ctx->ssl_ctx_noverify = cfg->libs_ctx->ssl_ctx_noverify; + } + else { + ctx->ssl_ctx = rspamd_init_ssl_ctx(); + ctx->ssl_ctx_noverify = rspamd_init_ssl_ctx_noverify(); + } + + ctx->event_loop = ev_base; + + ctx->keep_alive_hash = kh_init(rspamd_keep_alive_hash); + + return ctx; +} + +static void +rspamd_http_context_parse_proxy(struct rspamd_http_context *ctx, + const gchar *name, + struct upstream_list **pls) +{ + struct http_parser_url u; + struct upstream_list *uls; + + if (!ctx->ups_ctx) { + msg_err("cannot parse http_proxy %s - upstreams context is undefined", name); + return; + } + + memset(&u, 0, sizeof(u)); + + if (http_parser_parse_url(name, strlen(name), 1, &u) == 0) { + if (!(u.field_set & (1u << UF_HOST)) || u.port == 0) { + msg_err("cannot parse http(s) proxy %s - invalid host or port", name); + + return; + } + + uls = rspamd_upstreams_create(ctx->ups_ctx); + + if (!rspamd_upstreams_parse_line_len(uls, + name + u.field_data[UF_HOST].off, + u.field_data[UF_HOST].len, u.port, NULL)) { + msg_err("cannot parse http(s) proxy %s - invalid data", name); + + rspamd_upstreams_destroy(uls); + } + else { + *pls = uls; + msg_info("set http(s) proxy to %s", name); + } + } + else { + uls = rspamd_upstreams_create(ctx->ups_ctx); + + if (!rspamd_upstreams_parse_line(uls, + name, 3128, NULL)) { + msg_err("cannot parse http(s) proxy %s - invalid data", name); + + rspamd_upstreams_destroy(uls); + } + else { + *pls = uls; + msg_info("set http(s) proxy to %s", name); + } + } +} + +static void +rspamd_http_context_init(struct rspamd_http_context *ctx) +{ + if (ctx->config.kp_cache_size_client > 0) { + ctx->client_kp_cache = rspamd_keypair_cache_new(ctx->config.kp_cache_size_client); + } + + if (ctx->config.kp_cache_size_server > 0) { + ctx->server_kp_cache = rspamd_keypair_cache_new(ctx->config.kp_cache_size_server); + } + + if (ctx->config.client_key_rotate_time > 0 && ctx->event_loop) { + double jittered = rspamd_time_jitter(ctx->config.client_key_rotate_time, + 0); + + ev_timer_init(&ctx->client_rotate_ev, + rspamd_http_context_client_rotate_ev, jittered, 0); + ev_timer_start(ctx->event_loop, &ctx->client_rotate_ev); + ctx->client_rotate_ev.data = ctx; + } + + if (ctx->config.http_proxy) { + rspamd_http_context_parse_proxy(ctx, ctx->config.http_proxy, + &ctx->http_proxies); + } + + default_ctx = ctx; +} + +struct rspamd_http_context * +rspamd_http_context_create(struct rspamd_config *cfg, + struct ev_loop *ev_base, + struct upstream_ctx *ups_ctx) +{ + struct rspamd_http_context *ctx; + const ucl_object_t *http_obj; + + ctx = rspamd_http_context_new_default(cfg, ev_base, ups_ctx); + http_obj = ucl_object_lookup(cfg->cfg_ucl_obj, "http"); + + if (http_obj) { + const ucl_object_t *server_obj, *client_obj; + + client_obj = ucl_object_lookup(http_obj, "client"); + + if (client_obj) { + const ucl_object_t *kp_size; + + kp_size = ucl_object_lookup(client_obj, "cache_size"); + + if (kp_size) { + ctx->config.kp_cache_size_client = ucl_object_toint(kp_size); + } + + const ucl_object_t *rotate_time; + + rotate_time = ucl_object_lookup(client_obj, "rotate_time"); + + if (rotate_time) { + ctx->config.client_key_rotate_time = ucl_object_todouble(rotate_time); + } + + const ucl_object_t *user_agent; + + user_agent = ucl_object_lookup(client_obj, "user_agent"); + + if (user_agent) { + ctx->config.user_agent = ucl_object_tostring(user_agent); + + if (ctx->config.user_agent && strlen(ctx->config.user_agent) == 0) { + ctx->config.user_agent = NULL; + } + } + + const ucl_object_t *server_hdr; + server_hdr = ucl_object_lookup(client_obj, "server_hdr"); + + if (server_hdr) { + ctx->config.server_hdr = ucl_object_tostring(server_hdr); + + if (ctx->config.server_hdr && strlen(ctx->config.server_hdr) == 0) { + ctx->config.server_hdr = ""; + } + } + + const ucl_object_t *keepalive_interval; + + keepalive_interval = ucl_object_lookup(client_obj, "keepalive_interval"); + + if (keepalive_interval) { + ctx->config.keepalive_interval = ucl_object_todouble(keepalive_interval); + } + + const ucl_object_t *http_proxy; + http_proxy = ucl_object_lookup(client_obj, "http_proxy"); + + if (http_proxy) { + ctx->config.http_proxy = ucl_object_tostring(http_proxy); + } + } + + server_obj = ucl_object_lookup(http_obj, "server"); + + if (server_obj) { + const ucl_object_t *kp_size; + + kp_size = ucl_object_lookup(server_obj, "cache_size"); + + if (kp_size) { + ctx->config.kp_cache_size_server = ucl_object_toint(kp_size); + } + } + } + + rspamd_http_context_init(ctx); + + return ctx; +} + + +void rspamd_http_context_free(struct rspamd_http_context *ctx) +{ + if (ctx == default_ctx) { + default_ctx = NULL; + } + + if (ctx->client_kp_cache) { + rspamd_keypair_cache_destroy(ctx->client_kp_cache); + } + + if (ctx->server_kp_cache) { + rspamd_keypair_cache_destroy(ctx->server_kp_cache); + } + + if (ctx->config.client_key_rotate_time > 0) { + ev_timer_stop(ctx->event_loop, &ctx->client_rotate_ev); + + if (ctx->client_kp) { + rspamd_keypair_unref(ctx->client_kp); + } + } + + struct rspamd_keepalive_hash_key *hk; + + kh_foreach_key(ctx->keep_alive_hash, hk, { + msg_debug_http_context("cleanup keepalive elt %s (%s)", + rspamd_inet_address_to_string_pretty(hk->addr), + hk->host); + + if (hk->host) { + g_free(hk->host); + } + + rspamd_inet_address_free(hk->addr); + rspamd_http_keepalive_queue_cleanup(&hk->conns); + g_free(hk); + }); + + kh_destroy(rspamd_keep_alive_hash, ctx->keep_alive_hash); + + if (ctx->http_proxies) { + rspamd_upstreams_destroy(ctx->http_proxies); + } + + g_free(ctx); +} + +struct rspamd_http_context * +rspamd_http_context_create_config(struct rspamd_http_context_cfg *cfg, + struct ev_loop *ev_base, + struct upstream_ctx *ups_ctx) +{ + struct rspamd_http_context *ctx; + + ctx = rspamd_http_context_new_default(NULL, ev_base, ups_ctx); + memcpy(&ctx->config, cfg, sizeof(*cfg)); + rspamd_http_context_init(ctx); + + return ctx; +} + +struct rspamd_http_context * +rspamd_http_context_default(void) +{ + g_assert(default_ctx != NULL); + + return default_ctx; +} + +gint32 +rspamd_keep_alive_key_hash(struct rspamd_keepalive_hash_key *k) +{ + rspamd_cryptobox_fast_hash_state_t hst; + + rspamd_cryptobox_fast_hash_init(&hst, 0); + + if (k->host) { + rspamd_cryptobox_fast_hash_update(&hst, k->host, strlen(k->host)); + } + + rspamd_cryptobox_fast_hash_update(&hst, &k->port, sizeof(k->port)); + rspamd_cryptobox_fast_hash_update(&hst, &k->is_ssl, sizeof(k->is_ssl)); + + return rspamd_cryptobox_fast_hash_final(&hst); +} + +bool rspamd_keep_alive_key_equal(struct rspamd_keepalive_hash_key *k1, + struct rspamd_keepalive_hash_key *k2) +{ + if (k1->is_ssl != k2->is_ssl) { + return false; + } + + if (k1->host && k2->host) { + if (k1->port == k2->port) { + return strcmp(k1->host, k2->host) == 0; + } + } + else if (!k1->host && !k2->host) { + return (k1->port == k2->port); + } + + /* One has host and another has no host */ + return false; +} + +struct rspamd_http_connection * +rspamd_http_context_check_keepalive(struct rspamd_http_context *ctx, + const rspamd_inet_addr_t *addr, + const gchar *host, + bool is_ssl) +{ + struct rspamd_keepalive_hash_key hk, *phk; + khiter_t k; + + if (ctx == NULL) { + ctx = rspamd_http_context_default(); + } + + hk.addr = (rspamd_inet_addr_t *) addr; + hk.host = (gchar *) host; + hk.port = rspamd_inet_address_get_port(addr); + hk.is_ssl = is_ssl; + + k = kh_get(rspamd_keep_alive_hash, ctx->keep_alive_hash, &hk); + + if (k != kh_end(ctx->keep_alive_hash)) { + phk = kh_key(ctx->keep_alive_hash, k); + GQueue *conns = &phk->conns; + + /* Use stack based approach */ + + if (g_queue_get_length(conns) > 0) { + struct rspamd_http_keepalive_cbdata *cbd; + struct rspamd_http_connection *conn; + gint err; + socklen_t len = sizeof(gint); + + cbd = g_queue_pop_head(conns); + rspamd_ev_watcher_stop(ctx->event_loop, &cbd->ev); + conn = cbd->conn; + g_free(cbd); + + if (getsockopt(conn->fd, SOL_SOCKET, SO_ERROR, (void *) &err, &len) == -1) { + err = errno; + } + + if (err != 0) { + rspamd_http_connection_unref(conn); + + msg_debug_http_context("invalid reused keepalive element %s (%s, ssl=%d); " + "%s error; " + "%d connections queued", + rspamd_inet_address_to_string_pretty(phk->addr), + phk->host, + (int) phk->is_ssl, + g_strerror(err), + conns->length); + + return NULL; + } + + msg_debug_http_context("reused keepalive element %s (%s, ssl=%d), %d connections queued", + rspamd_inet_address_to_string_pretty(phk->addr), + phk->host, + (int) phk->is_ssl, + conns->length); + + /* We transfer refcount here! */ + return conn; + } + else { + msg_debug_http_context("found empty keepalive element %s (%s), cannot reuse", + rspamd_inet_address_to_string_pretty(phk->addr), + phk->host); + } + } + + return NULL; +} + +const rspamd_inet_addr_t * +rspamd_http_context_has_keepalive(struct rspamd_http_context *ctx, + const gchar *host, + unsigned port, + bool is_ssl) +{ + struct rspamd_keepalive_hash_key hk, *phk; + khiter_t k; + + if (ctx == NULL) { + ctx = rspamd_http_context_default(); + } + + hk.host = (gchar *) host; + hk.port = port; + hk.is_ssl = is_ssl; + + k = kh_get(rspamd_keep_alive_hash, ctx->keep_alive_hash, &hk); + + if (k != kh_end(ctx->keep_alive_hash)) { + phk = kh_key(ctx->keep_alive_hash, k); + GQueue *conns = &phk->conns; + + if (g_queue_get_length(conns) > 0) { + return phk->addr; + } + } + + return NULL; +} + +void rspamd_http_context_prepare_keepalive(struct rspamd_http_context *ctx, + struct rspamd_http_connection *conn, + const rspamd_inet_addr_t *addr, + const gchar *host, + bool is_ssl) +{ + struct rspamd_keepalive_hash_key hk, *phk; + khiter_t k; + + hk.addr = (rspamd_inet_addr_t *) addr; + hk.host = (gchar *) host; + hk.is_ssl = is_ssl; + hk.port = rspamd_inet_address_get_port(addr); + + k = kh_get(rspamd_keep_alive_hash, ctx->keep_alive_hash, &hk); + + if (k != kh_end(ctx->keep_alive_hash)) { + /* Reuse existing */ + conn->keepalive_hash_key = kh_key(ctx->keep_alive_hash, k); + msg_debug_http_context("use existing keepalive element %s (%s)", + rspamd_inet_address_to_string_pretty(conn->keepalive_hash_key->addr), + conn->keepalive_hash_key->host); + } + else { + /* Create new one */ + GQueue empty_init = G_QUEUE_INIT; + gint r; + + phk = g_malloc(sizeof(*phk)); + phk->conns = empty_init; + phk->host = g_strdup(host); + phk->is_ssl = is_ssl; + phk->addr = rspamd_inet_address_copy(addr, NULL); + phk->port = hk.port; + + + kh_put(rspamd_keep_alive_hash, ctx->keep_alive_hash, phk, &r); + conn->keepalive_hash_key = phk; + + msg_debug_http_context("create new keepalive element %s (%s)", + rspamd_inet_address_to_string_pretty(conn->keepalive_hash_key->addr), + conn->keepalive_hash_key->host); + } +} + +static void +rspamd_http_keepalive_handler(gint fd, short what, gpointer ud) +{ + struct rspamd_http_keepalive_cbdata *cbdata = + (struct rspamd_http_keepalive_cbdata *) ud; /* + * We can get here if a remote side reported something or it has + * timed out. In both cases we just terminate keepalive connection. + */ + + g_queue_delete_link(cbdata->queue, cbdata->link); + msg_debug_http_context("remove keepalive element %s (%s), %d connections left", + rspamd_inet_address_to_string_pretty(cbdata->conn->keepalive_hash_key->addr), + cbdata->conn->keepalive_hash_key->host, + cbdata->queue->length); + /* unref call closes fd, so we need to remove ev watcher first! */ + rspamd_ev_watcher_stop(cbdata->ctx->event_loop, &cbdata->ev); + rspamd_http_connection_unref(cbdata->conn); + g_free(cbdata); +} + +/* Non-static for unit testing */ +long rspamd_http_parse_keepalive_timeout(const rspamd_ftok_t *tok) +{ + long timeout = -1; + goffset pos = rspamd_substring_search(tok->begin, + tok->len, "timeout", sizeof("timeout") - 1); + + if (pos != -1) { + pos += sizeof("timeout") - 1; + + /* Skip spaces and equal sign */ + while (pos < tok->len) { + if (tok->begin[pos] != '=' && !g_ascii_isspace(tok->begin[pos])) { + break; + } + pos++; + } + + gsize ndigits = rspamd_memspn(tok->begin + pos, "0123456789", tok->len - pos); + glong real_timeout; + + if (ndigits > 0) { + if (rspamd_strtoul(tok->begin + pos, ndigits, &real_timeout)) { + timeout = real_timeout; + msg_debug_http_context("got timeout attr %l", timeout); + } + } + } + + return timeout; +} + +void rspamd_http_context_push_keepalive(struct rspamd_http_context *ctx, + struct rspamd_http_connection *conn, + struct rspamd_http_message *msg, + struct ev_loop *event_loop) +{ + struct rspamd_http_keepalive_cbdata *cbdata; + gdouble timeout = ctx->config.keepalive_interval; + + g_assert(conn->keepalive_hash_key != NULL); + + if (msg) { + const rspamd_ftok_t *tok; + rspamd_ftok_t cmp; + + tok = rspamd_http_message_find_header(msg, "Connection"); + + if (!tok) { + /* Server has not stated that it can do keep alive */ + conn->finished = TRUE; + msg_debug_http_context("no Connection header"); + return; + } + + RSPAMD_FTOK_ASSIGN(&cmp, "keep-alive"); + + if (rspamd_ftok_casecmp(&cmp, tok) != 0) { + conn->finished = TRUE; + msg_debug_http_context("connection header is not `keep-alive`"); + return; + } + + /* We can proceed, check timeout */ + + tok = rspamd_http_message_find_header(msg, "Keep-Alive"); + + if (tok) { + long maybe_timeout = rspamd_http_parse_keepalive_timeout(tok); + + if (maybe_timeout > 0) { + timeout = maybe_timeout; + } + } + } + + /* Move connection to the keepalive pool */ + cbdata = g_malloc0(sizeof(*cbdata)); + + cbdata->conn = rspamd_http_connection_ref(conn); + /* Use stack like approach to that would easy reading */ + g_queue_push_head(&conn->keepalive_hash_key->conns, cbdata); + cbdata->link = conn->keepalive_hash_key->conns.head; + + cbdata->queue = &conn->keepalive_hash_key->conns; + cbdata->ctx = ctx; + conn->finished = FALSE; + + rspamd_ev_watcher_init(&cbdata->ev, conn->fd, EV_READ, + rspamd_http_keepalive_handler, + cbdata); + rspamd_ev_watcher_start(event_loop, &cbdata->ev, timeout); + + msg_debug_http_context("push keepalive element %s (%s), %d connections queued, %.1f timeout", + rspamd_inet_address_to_string_pretty(cbdata->conn->keepalive_hash_key->addr), + cbdata->conn->keepalive_hash_key->host, + cbdata->queue->length, + timeout); +}
\ No newline at end of file diff --git a/src/libserver/http/http_context.h b/src/libserver/http/http_context.h new file mode 100644 index 0000000..f3622ae --- /dev/null +++ b/src/libserver/http/http_context.h @@ -0,0 +1,122 @@ +/*- + * Copyright 2019 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RSPAMD_HTTP_CONTEXT_H +#define RSPAMD_HTTP_CONTEXT_H + +#include "config.h" +#include "ucl.h" +#include "addr.h" + +#include "contrib/libev/ev.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct rspamd_http_context; +struct rspamd_config; +struct rspamd_http_message; +struct upstream_ctx; + +struct rspamd_http_context_cfg { + guint kp_cache_size_client; + guint kp_cache_size_server; + guint ssl_cache_size; + gdouble keepalive_interval; + gdouble client_key_rotate_time; + const gchar *user_agent; + const gchar *http_proxy; + const gchar *server_hdr; +}; + +/** + * Creates and configures new HTTP context + * @param root_conf configuration object + * @param ev_base event base + * @return new context used for both client and server HTTP connections + */ +struct rspamd_http_context *rspamd_http_context_create(struct rspamd_config *cfg, + struct ev_loop *ev_base, + struct upstream_ctx *ctx); + +struct rspamd_http_context *rspamd_http_context_create_config( + struct rspamd_http_context_cfg *cfg, + struct ev_loop *ev_base, + struct upstream_ctx *ctx); + +/** + * Destroys context + * @param ctx + */ +void rspamd_http_context_free(struct rspamd_http_context *ctx); + +struct rspamd_http_context *rspamd_http_context_default(void); + +/** + * Returns preserved keepalive connection if it's available. + * Refcount is transferred to caller! + * @param ctx + * @param addr + * @param host + * @return + */ +struct rspamd_http_connection *rspamd_http_context_check_keepalive(struct rspamd_http_context *ctx, + const rspamd_inet_addr_t *addr, + const gchar *host, + bool is_ssl); + +/** + * Checks if there is a valid keepalive connection + * @param ctx + * @param addr + * @param host + * @param is_ssl + * @return + */ +const rspamd_inet_addr_t *rspamd_http_context_has_keepalive(struct rspamd_http_context *ctx, + const gchar *host, + unsigned port, + bool is_ssl); + +/** + * Prepares keepalive key for a connection by creating a new entry or by reusing existent + * Bear in mind, that keepalive pool has currently no cleanup methods! + * @param ctx + * @param conn + * @param addr + * @param host + */ +void rspamd_http_context_prepare_keepalive(struct rspamd_http_context *ctx, struct rspamd_http_connection *conn, + const rspamd_inet_addr_t *addr, const gchar *host, bool is_ssl); + +/** + * Pushes a connection to keepalive pool after client request is finished, + * keepalive key *must* be prepared before using of this function + * @param ctx + * @param conn + * @param msg + */ +void rspamd_http_context_push_keepalive(struct rspamd_http_context *ctx, + struct rspamd_http_connection *conn, + struct rspamd_http_message *msg, + struct ev_loop *ev_base); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/libserver/http/http_message.c b/src/libserver/http/http_message.c new file mode 100644 index 0000000..670122d --- /dev/null +++ b/src/libserver/http/http_message.c @@ -0,0 +1,725 @@ +/*- + * Copyright 2019 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "http_message.h" +#include "http_connection.h" +#include "http_private.h" +#include "libutil/printf.h" +#include "libserver/logger.h" +#include "utlist.h" +#include "unix-std.h" + +struct rspamd_http_message * +rspamd_http_new_message(enum rspamd_http_message_type type) +{ + struct rspamd_http_message *new; + + new = g_malloc0(sizeof(struct rspamd_http_message)); + + if (type == HTTP_REQUEST) { + new->url = rspamd_fstring_new(); + } + else { + new->url = NULL; + new->code = 200; + } + + new->port = 80; + new->type = type; + new->method = HTTP_INVALID; + new->headers = kh_init(rspamd_http_headers_hash); + + REF_INIT_RETAIN(new, rspamd_http_message_free); + + return new; +} + +struct rspamd_http_message * +rspamd_http_message_from_url(const gchar *url) +{ + struct http_parser_url pu; + struct rspamd_http_message *msg; + const gchar *host, *path; + size_t pathlen, urllen; + guint flags = 0; + + if (url == NULL) { + return NULL; + } + + urllen = strlen(url); + memset(&pu, 0, sizeof(pu)); + + if (http_parser_parse_url(url, urllen, FALSE, &pu) != 0) { + msg_warn("cannot parse URL: %s", url); + return NULL; + } + + if ((pu.field_set & (1 << UF_HOST)) == 0) { + msg_warn("no host argument in URL: %s", url); + return NULL; + } + + if ((pu.field_set & (1 << UF_SCHEMA))) { + if (pu.field_data[UF_SCHEMA].len == sizeof("https") - 1 && + memcmp(url + pu.field_data[UF_SCHEMA].off, "https", 5) == 0) { + flags |= RSPAMD_HTTP_FLAG_WANT_SSL; + } + } + + if ((pu.field_set & (1 << UF_PATH)) == 0) { + path = "/"; + pathlen = 1; + } + else { + path = url + pu.field_data[UF_PATH].off; + pathlen = urllen - pu.field_data[UF_PATH].off; + } + + msg = rspamd_http_new_message(HTTP_REQUEST); + host = url + pu.field_data[UF_HOST].off; + msg->flags = flags; + + if ((pu.field_set & (1 << UF_PORT)) != 0) { + msg->port = pu.port; + } + else { + /* XXX: magic constant */ + if (flags & RSPAMD_HTTP_FLAG_WANT_SSL) { + msg->port = 443; + } + else { + msg->port = 80; + } + } + + msg->host = g_string_new_len(host, pu.field_data[UF_HOST].len); + msg->url = rspamd_fstring_append(msg->url, path, pathlen); + + REF_INIT_RETAIN(msg, rspamd_http_message_free); + + return msg; +} + +const gchar * +rspamd_http_message_get_body(struct rspamd_http_message *msg, + gsize *blen) +{ + const gchar *ret = NULL; + + if (msg->body_buf.len > 0) { + ret = msg->body_buf.begin; + } + + if (blen) { + *blen = msg->body_buf.len; + } + + return ret; +} + +static void +rspamd_http_shname_dtor(void *p) +{ + struct rspamd_storage_shmem *n = p; + +#ifdef HAVE_SANE_SHMEM + shm_unlink(n->shm_name); +#else + unlink(n->shm_name); +#endif + g_free(n->shm_name); + g_free(n); +} + +struct rspamd_storage_shmem * +rspamd_http_message_shmem_ref(struct rspamd_http_message *msg) +{ + if ((msg->flags & RSPAMD_HTTP_FLAG_SHMEM) && msg->body_buf.c.shared.name) { + REF_RETAIN(msg->body_buf.c.shared.name); + return msg->body_buf.c.shared.name; + } + + return NULL; +} + +guint rspamd_http_message_get_flags(struct rspamd_http_message *msg) +{ + return msg->flags; +} + +void rspamd_http_message_shmem_unref(struct rspamd_storage_shmem *p) +{ + REF_RELEASE(p); +} + +gboolean +rspamd_http_message_set_body(struct rspamd_http_message *msg, + const gchar *data, gsize len) +{ + union _rspamd_storage_u *storage; + storage = &msg->body_buf.c; + + rspamd_http_message_storage_cleanup(msg); + + if (msg->flags & RSPAMD_HTTP_FLAG_SHMEM) { + storage->shared.name = g_malloc(sizeof(*storage->shared.name)); + REF_INIT_RETAIN(storage->shared.name, rspamd_http_shname_dtor); +#ifdef HAVE_SANE_SHMEM +#if defined(__DragonFly__) + // DragonFly uses regular files for shm. User rspamd is not allowed to create + // files in the root. + storage->shared.name->shm_name = g_strdup("/tmp/rhm.XXXXXXXXXXXXXXXXXXXX"); +#else + storage->shared.name->shm_name = g_strdup("/rhm.XXXXXXXXXXXXXXXXXXXX"); +#endif + storage->shared.shm_fd = rspamd_shmem_mkstemp(storage->shared.name->shm_name); +#else + /* XXX: assume that tempdir is /tmp */ + storage->shared.name->shm_name = g_strdup("/tmp/rhm.XXXXXXXXXXXXXXXXXXXX"); + storage->shared.shm_fd = mkstemp(storage->shared.name->shm_name); +#endif + + if (storage->shared.shm_fd == -1) { + return FALSE; + } + + if (len != 0 && len != G_MAXSIZE) { + if (ftruncate(storage->shared.shm_fd, len) == -1) { + return FALSE; + } + + msg->body_buf.str = mmap(NULL, len, + PROT_WRITE | PROT_READ, MAP_SHARED, + storage->shared.shm_fd, 0); + + if (msg->body_buf.str == MAP_FAILED) { + return FALSE; + } + + msg->body_buf.begin = msg->body_buf.str; + msg->body_buf.allocated_len = len; + + if (data != NULL) { + memcpy(msg->body_buf.str, data, len); + msg->body_buf.len = len; + } + } + else { + msg->body_buf.len = 0; + msg->body_buf.begin = NULL; + msg->body_buf.str = NULL; + msg->body_buf.allocated_len = 0; + } + } + else { + if (len != 0 && len != G_MAXSIZE) { + if (data == NULL) { + storage->normal = rspamd_fstring_sized_new(len); + msg->body_buf.len = 0; + } + else { + storage->normal = rspamd_fstring_new_init(data, len); + msg->body_buf.len = len; + } + } + else { + storage->normal = rspamd_fstring_new(); + } + + msg->body_buf.begin = storage->normal->str; + msg->body_buf.str = storage->normal->str; + msg->body_buf.allocated_len = storage->normal->allocated; + } + + msg->flags |= RSPAMD_HTTP_FLAG_HAS_BODY; + + return TRUE; +} + +void rspamd_http_message_set_method(struct rspamd_http_message *msg, + const gchar *method) +{ + gint i; + + /* Linear search: not very efficient method */ + for (i = 0; i < HTTP_METHOD_MAX; i++) { + if (g_ascii_strcasecmp(method, http_method_str(i)) == 0) { + msg->method = i; + } + } +} + +gboolean +rspamd_http_message_set_body_from_fd(struct rspamd_http_message *msg, + gint fd) +{ + union _rspamd_storage_u *storage; + struct stat st; + + rspamd_http_message_storage_cleanup(msg); + + storage = &msg->body_buf.c; + msg->flags |= RSPAMD_HTTP_FLAG_SHMEM | RSPAMD_HTTP_FLAG_SHMEM_IMMUTABLE; + + storage->shared.shm_fd = dup(fd); + msg->body_buf.str = MAP_FAILED; + + if (storage->shared.shm_fd == -1) { + return FALSE; + } + + if (fstat(storage->shared.shm_fd, &st) == -1) { + return FALSE; + } + + msg->body_buf.str = mmap(NULL, st.st_size, + PROT_READ, MAP_SHARED, + storage->shared.shm_fd, 0); + + if (msg->body_buf.str == MAP_FAILED) { + return FALSE; + } + + msg->body_buf.begin = msg->body_buf.str; + msg->body_buf.len = st.st_size; + msg->body_buf.allocated_len = st.st_size; + + return TRUE; +} + +gboolean +rspamd_http_message_set_body_from_fstring_steal(struct rspamd_http_message *msg, + rspamd_fstring_t *fstr) +{ + union _rspamd_storage_u *storage; + + rspamd_http_message_storage_cleanup(msg); + + storage = &msg->body_buf.c; + msg->flags &= ~(RSPAMD_HTTP_FLAG_SHMEM | RSPAMD_HTTP_FLAG_SHMEM_IMMUTABLE); + + storage->normal = fstr; + msg->body_buf.str = fstr->str; + msg->body_buf.begin = msg->body_buf.str; + msg->body_buf.len = fstr->len; + msg->body_buf.allocated_len = fstr->allocated; + + return TRUE; +} + +gboolean +rspamd_http_message_set_body_from_fstring_copy(struct rspamd_http_message *msg, + const rspamd_fstring_t *fstr) +{ + union _rspamd_storage_u *storage; + + rspamd_http_message_storage_cleanup(msg); + + storage = &msg->body_buf.c; + msg->flags &= ~(RSPAMD_HTTP_FLAG_SHMEM | RSPAMD_HTTP_FLAG_SHMEM_IMMUTABLE); + + storage->normal = rspamd_fstring_new_init(fstr->str, fstr->len); + msg->body_buf.str = storage->normal->str; + msg->body_buf.begin = msg->body_buf.str; + msg->body_buf.len = storage->normal->len; + msg->body_buf.allocated_len = storage->normal->allocated; + + return TRUE; +} + + +gboolean +rspamd_http_message_grow_body(struct rspamd_http_message *msg, gsize len) +{ + struct stat st; + union _rspamd_storage_u *storage; + gsize newlen; + + storage = &msg->body_buf.c; + + if (msg->flags & RSPAMD_HTTP_FLAG_SHMEM) { + if (storage->shared.shm_fd == -1) { + return FALSE; + } + + if (fstat(storage->shared.shm_fd, &st) == -1) { + return FALSE; + } + + /* Check if we need to grow */ + if ((gsize) st.st_size < msg->body_buf.len + len) { + /* Need to grow */ + newlen = rspamd_fstring_suggest_size(msg->body_buf.len, st.st_size, + len); + /* Unmap as we need another size of segment */ + if (msg->body_buf.str != MAP_FAILED) { + munmap(msg->body_buf.str, st.st_size); + } + + if (ftruncate(storage->shared.shm_fd, newlen) == -1) { + return FALSE; + } + + msg->body_buf.str = mmap(NULL, newlen, + PROT_WRITE | PROT_READ, MAP_SHARED, + storage->shared.shm_fd, 0); + if (msg->body_buf.str == MAP_FAILED) { + return FALSE; + } + + msg->body_buf.begin = msg->body_buf.str; + msg->body_buf.allocated_len = newlen; + } + } + else { + storage->normal = rspamd_fstring_grow(storage->normal, len); + + /* Append might cause realloc */ + msg->body_buf.begin = storage->normal->str; + msg->body_buf.len = storage->normal->len; + msg->body_buf.str = storage->normal->str; + msg->body_buf.allocated_len = storage->normal->allocated; + } + + return TRUE; +} + +gboolean +rspamd_http_message_append_body(struct rspamd_http_message *msg, + const gchar *data, gsize len) +{ + union _rspamd_storage_u *storage; + + storage = &msg->body_buf.c; + + if (msg->flags & RSPAMD_HTTP_FLAG_SHMEM) { + if (!rspamd_http_message_grow_body(msg, len)) { + return FALSE; + } + + memcpy(msg->body_buf.str + msg->body_buf.len, data, len); + msg->body_buf.len += len; + } + else { + storage->normal = rspamd_fstring_append(storage->normal, data, len); + + /* Append might cause realloc */ + msg->body_buf.begin = storage->normal->str; + msg->body_buf.len = storage->normal->len; + msg->body_buf.str = storage->normal->str; + msg->body_buf.allocated_len = storage->normal->allocated; + } + + return TRUE; +} + +void rspamd_http_message_storage_cleanup(struct rspamd_http_message *msg) +{ + union _rspamd_storage_u *storage; + struct stat st; + + if (msg->flags & RSPAMD_HTTP_FLAG_SHMEM) { + storage = &msg->body_buf.c; + + if (storage->shared.shm_fd > 0) { + g_assert(fstat(storage->shared.shm_fd, &st) != -1); + + if (msg->body_buf.str != MAP_FAILED) { + munmap(msg->body_buf.str, st.st_size); + } + + close(storage->shared.shm_fd); + } + + if (storage->shared.name != NULL) { + REF_RELEASE(storage->shared.name); + } + + storage->shared.shm_fd = -1; + msg->body_buf.str = MAP_FAILED; + } + else { + if (msg->body_buf.c.normal) { + rspamd_fstring_free(msg->body_buf.c.normal); + } + + msg->body_buf.c.normal = NULL; + } + + msg->body_buf.len = 0; +} + +void rspamd_http_message_free(struct rspamd_http_message *msg) +{ + struct rspamd_http_header *hdr, *hcur, *hcurtmp; + + kh_foreach_value (msg->headers, hdr, { + DL_FOREACH_SAFE (hdr, hcur, hcurtmp) { + rspamd_fstring_free (hcur->combined); + g_free (hcur); +} +}); + +kh_destroy(rspamd_http_headers_hash, msg->headers); +rspamd_http_message_storage_cleanup(msg); + +if (msg->url != NULL) { + rspamd_fstring_free(msg->url); +} +if (msg->status != NULL) { + rspamd_fstring_free(msg->status); +} +if (msg->host != NULL) { + g_string_free(msg->host, TRUE); +} +if (msg->peer_key != NULL) { + rspamd_pubkey_unref(msg->peer_key); +} + +g_free(msg); +} + +void rspamd_http_message_set_peer_key(struct rspamd_http_message *msg, + struct rspamd_cryptobox_pubkey *pk) +{ + if (msg->peer_key != NULL) { + rspamd_pubkey_unref(msg->peer_key); + } + + if (pk) { + msg->peer_key = rspamd_pubkey_ref(pk); + } + else { + msg->peer_key = NULL; + } +} + +void rspamd_http_message_add_header_len(struct rspamd_http_message *msg, + const gchar *name, + const gchar *value, + gsize len) +{ + struct rspamd_http_header *hdr, *found; + guint nlen, vlen; + khiter_t k; + gint r; + + if (msg != NULL && name != NULL && value != NULL) { + hdr = g_malloc0(sizeof(struct rspamd_http_header)); + nlen = strlen(name); + vlen = len; + + if (g_ascii_strcasecmp(name, "host") == 0) { + msg->flags |= RSPAMD_HTTP_FLAG_HAS_HOST_HEADER; + } + + hdr->combined = rspamd_fstring_sized_new(nlen + vlen + 4); + rspamd_printf_fstring(&hdr->combined, "%s: %*s\r\n", name, (gint) vlen, + value); + hdr->name.begin = hdr->combined->str; + hdr->name.len = nlen; + hdr->value.begin = hdr->combined->str + nlen + 2; + hdr->value.len = vlen; + + k = kh_put(rspamd_http_headers_hash, msg->headers, &hdr->name, + &r); + + if (r != 0) { + kh_value(msg->headers, k) = hdr; + found = NULL; + } + else { + found = kh_value(msg->headers, k); + } + + DL_APPEND(found, hdr); + } +} + +void rspamd_http_message_add_header(struct rspamd_http_message *msg, + const gchar *name, + const gchar *value) +{ + if (value) { + rspamd_http_message_add_header_len(msg, name, value, strlen(value)); + } +} + +void rspamd_http_message_add_header_fstr(struct rspamd_http_message *msg, + const gchar *name, + rspamd_fstring_t *value) +{ + struct rspamd_http_header *hdr, *found = NULL; + guint nlen, vlen; + khiter_t k; + gint r; + + if (msg != NULL && name != NULL && value != NULL) { + hdr = g_malloc0(sizeof(struct rspamd_http_header)); + nlen = strlen(name); + vlen = value->len; + hdr->combined = rspamd_fstring_sized_new(nlen + vlen + 4); + rspamd_printf_fstring(&hdr->combined, "%s: %V\r\n", name, value); + hdr->name.begin = hdr->combined->str; + hdr->name.len = nlen; + hdr->value.begin = hdr->combined->str + nlen + 2; + hdr->value.len = vlen; + + k = kh_put(rspamd_http_headers_hash, msg->headers, &hdr->name, + &r); + + if (r != 0) { + kh_value(msg->headers, k) = hdr; + found = NULL; + } + else { + found = kh_value(msg->headers, k); + } + + DL_APPEND(found, hdr); + } +} + +const rspamd_ftok_t * +rspamd_http_message_find_header(struct rspamd_http_message *msg, + const gchar *name) +{ + const rspamd_ftok_t *res = NULL; + rspamd_ftok_t srch; + guint slen = strlen(name); + khiter_t k; + + if (msg != NULL) { + srch.begin = name; + srch.len = slen; + + k = kh_get(rspamd_http_headers_hash, msg->headers, &srch); + + if (k != kh_end(msg->headers)) { + res = &(kh_value(msg->headers, k)->value); + } + } + + return res; +} + +GPtrArray * +rspamd_http_message_find_header_multiple( + struct rspamd_http_message *msg, + const gchar *name) +{ + GPtrArray *res = NULL; + struct rspamd_http_header *hdr, *cur; + rspamd_ftok_t srch; + khiter_t k; + guint cnt = 0; + + guint slen = strlen(name); + + if (msg != NULL) { + srch.begin = name; + srch.len = slen; + + k = kh_get(rspamd_http_headers_hash, msg->headers, &srch); + + if (k != kh_end(msg->headers)) { + hdr = kh_value(msg->headers, k); + + LL_COUNT(hdr, cur, cnt); + res = g_ptr_array_sized_new(cnt); + + LL_FOREACH(hdr, cur) + { + g_ptr_array_add(res, &cur->value); + } + } + } + + + return res; +} + + +gboolean +rspamd_http_message_remove_header(struct rspamd_http_message *msg, + const gchar *name) +{ + struct rspamd_http_header *hdr, *hcur, *hcurtmp; + gboolean res = FALSE; + guint slen = strlen(name); + rspamd_ftok_t srch; + khiter_t k; + + if (msg != NULL) { + srch.begin = name; + srch.len = slen; + + k = kh_get(rspamd_http_headers_hash, msg->headers, &srch); + + if (k != kh_end(msg->headers)) { + hdr = kh_value(msg->headers, k); + kh_del(rspamd_http_headers_hash, msg->headers, k); + res = TRUE; + + DL_FOREACH_SAFE(hdr, hcur, hcurtmp) + { + rspamd_fstring_free(hcur->combined); + g_free(hcur); + } + } + } + + return res; +} + +const gchar * +rspamd_http_message_get_http_host(struct rspamd_http_message *msg, + gsize *hostlen) +{ + if (msg->flags & RSPAMD_HTTP_FLAG_HAS_HOST_HEADER) { + rspamd_ftok_t srch; + + RSPAMD_FTOK_ASSIGN(&srch, "Host"); + + khiter_t k = kh_get(rspamd_http_headers_hash, msg->headers, &srch); + + if (k != kh_end(msg->headers)) { + *hostlen = (kh_value(msg->headers, k)->value).len; + return (kh_value(msg->headers, k)->value).begin; + } + else if (msg->host) { + *hostlen = msg->host->len; + return msg->host->str; + } + } + else { + if (msg->host) { + *hostlen = msg->host->len; + return msg->host->str; + } + } + + return NULL; +} + +bool rspamd_http_message_is_standard_port(struct rspamd_http_message *msg) +{ + if (msg->flags & RSPAMD_HTTP_FLAG_WANT_SSL) { + return msg->port == 443; + } + + return msg->port == 80; +}
\ No newline at end of file diff --git a/src/libserver/http/http_message.h b/src/libserver/http/http_message.h new file mode 100644 index 0000000..fa8ed04 --- /dev/null +++ b/src/libserver/http/http_message.h @@ -0,0 +1,254 @@ +/*- + * Copyright 2019 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef RSPAMD_HTTP_MESSAGE_H +#define RSPAMD_HTTP_MESSAGE_H + +#include "config.h" +#include "keypair.h" +#include "keypairs_cache.h" +#include "fstring.h" +#include "ref.h" + + +#ifdef __cplusplus +extern "C" { +#endif + +struct rspamd_http_connection; + +enum rspamd_http_message_type { + HTTP_REQUEST = 0, + HTTP_RESPONSE +}; + +/** + * Extract the current message from a connection to deal with separately + * @param conn + * @return + */ +struct rspamd_http_message *rspamd_http_connection_steal_msg( + struct rspamd_http_connection *conn); + +/** + * Copy the current message from a connection to deal with separately + * @param conn + * @return + */ +struct rspamd_http_message *rspamd_http_connection_copy_msg( + struct rspamd_http_message *msg, GError **err); + +/** + * Create new HTTP message + * @param type request or response + * @return new http message + */ +struct rspamd_http_message *rspamd_http_new_message(enum rspamd_http_message_type type); + +/** + * Increase refcount number for an HTTP message + * @param msg message to use + * @return + */ +struct rspamd_http_message *rspamd_http_message_ref(struct rspamd_http_message *msg); + +/** + * Decrease number of refcounts for http message + * @param msg + */ +void rspamd_http_message_unref(struct rspamd_http_message *msg); + +/** + * Sets a key for peer + * @param msg + * @param pk + */ +void rspamd_http_message_set_peer_key(struct rspamd_http_message *msg, + struct rspamd_cryptobox_pubkey *pk); + +/** + * Create HTTP message from URL + * @param url + * @return new message or NULL + */ +struct rspamd_http_message *rspamd_http_message_from_url(const gchar *url); + +/** + * Returns body for a message + * @param msg + * @param blen pointer where to save body length + * @return pointer to body start + */ +const gchar *rspamd_http_message_get_body(struct rspamd_http_message *msg, + gsize *blen); + +/** + * Set message's body from the string + * @param msg + * @param data + * @param len + * @return TRUE if a message's body has been set + */ +gboolean rspamd_http_message_set_body(struct rspamd_http_message *msg, + const gchar *data, gsize len); + +/** + * Set message's method by name + * @param msg + * @param method + */ +void rspamd_http_message_set_method(struct rspamd_http_message *msg, + const gchar *method); + +/** + * Maps fd as message's body + * @param msg + * @param fd + * @return TRUE if a message's body has been set + */ +gboolean rspamd_http_message_set_body_from_fd(struct rspamd_http_message *msg, + gint fd); + +/** + * Uses rspamd_fstring_t as message's body, string is consumed by this operation + * @param msg + * @param fstr + * @return TRUE if a message's body has been set + */ +gboolean rspamd_http_message_set_body_from_fstring_steal(struct rspamd_http_message *msg, + rspamd_fstring_t *fstr); + +/** + * Uses rspamd_fstring_t as message's body, string is copied by this operation + * @param msg + * @param fstr + * @return TRUE if a message's body has been set + */ +gboolean rspamd_http_message_set_body_from_fstring_copy(struct rspamd_http_message *msg, + const rspamd_fstring_t *fstr); + +/** + * Appends data to message's body + * @param msg + * @param data + * @param len + * @return TRUE if a message's body has been set + */ +gboolean rspamd_http_message_append_body(struct rspamd_http_message *msg, + const gchar *data, gsize len); + +/** + * Append a header to http message + * @param rep + * @param name + * @param value + */ +void rspamd_http_message_add_header(struct rspamd_http_message *msg, + const gchar *name, + const gchar *value); + +void rspamd_http_message_add_header_len(struct rspamd_http_message *msg, + const gchar *name, + const gchar *value, + gsize len); + +void rspamd_http_message_add_header_fstr(struct rspamd_http_message *msg, + const gchar *name, + rspamd_fstring_t *value); + +/** + * Search for a specified header in message + * @param msg message + * @param name name of header + */ +const rspamd_ftok_t *rspamd_http_message_find_header( + struct rspamd_http_message *msg, + const gchar *name); + +/** + * Search for a header that has multiple values + * @param msg + * @param name + * @return list of rspamd_ftok_t * with values + */ +GPtrArray *rspamd_http_message_find_header_multiple( + struct rspamd_http_message *msg, + const gchar *name); + +/** + * Remove specific header from a message + * @param msg + * @param name + * @return + */ +gboolean rspamd_http_message_remove_header(struct rspamd_http_message *msg, + const gchar *name); + +/** + * Free HTTP message + * @param msg + */ +void rspamd_http_message_free(struct rspamd_http_message *msg); + +/** + * Extract arguments from a message's URI contained inside query string decoding + * them if needed + * @param msg HTTP request message + * @return new GHashTable which maps rspamd_ftok_t* to rspamd_ftok_t* + * (table must be freed by a caller) + */ +GHashTable *rspamd_http_message_parse_query(struct rspamd_http_message *msg); + +/** + * Increase refcount for shared file (if any) to prevent early memory unlinking + * @param msg + */ +struct rspamd_storage_shmem *rspamd_http_message_shmem_ref(struct rspamd_http_message *msg); + +/** + * Decrease external ref for shmem segment associated with a message + * @param msg + */ +void rspamd_http_message_shmem_unref(struct rspamd_storage_shmem *p); + +/** + * Returns message's flags + * @param msg + * @return + */ +guint rspamd_http_message_get_flags(struct rspamd_http_message *msg); + +/** + * Returns an HTTP hostname for a message, derived from a header if it has it + * or from a url if it doesn't + * @param msg + * @param hostlen output of the host length + * @return + */ +const gchar *rspamd_http_message_get_http_host(struct rspamd_http_message *msg, + gsize *hostlen); + +/** + * Returns true if a message has standard port (80 or 443 for https) + * @param msg + * @return + */ +bool rspamd_http_message_is_standard_port(struct rspamd_http_message *msg); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/libserver/http/http_private.h b/src/libserver/http/http_private.h new file mode 100644 index 0000000..096545e --- /dev/null +++ b/src/libserver/http/http_private.h @@ -0,0 +1,129 @@ +/*- + * Copyright 2016 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef SRC_LIBUTIL_HTTP_PRIVATE_H_ +#define SRC_LIBUTIL_HTTP_PRIVATE_H_ + +#include "http_connection.h" +#include "http_parser.h" +#include "str_util.h" +#include "keypair.h" +#include "keypairs_cache.h" +#include "ref.h" +#include "upstream.h" +#include "khash.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * HTTP header structure + */ +struct rspamd_http_header { + rspamd_fstring_t *combined; + rspamd_ftok_t name; + rspamd_ftok_t value; + struct rspamd_http_header *prev, *next; +}; + +KHASH_INIT(rspamd_http_headers_hash, rspamd_ftok_t *, + struct rspamd_http_header *, 1, + rspamd_ftok_icase_hash, rspamd_ftok_icase_equal); + +/** + * HTTP message structure, used for requests and replies + */ +struct rspamd_http_message { + rspamd_fstring_t *url; + GString *host; + rspamd_fstring_t *status; + khash_t(rspamd_http_headers_hash) * headers; + + struct _rspamd_body_buf_s { + /* Data start */ + const gchar *begin; + /* Data len */ + gsize len; + /* Allocated len */ + gsize allocated_len; + /* Data buffer (used to write data inside) */ + gchar *str; + + /* Internal storage */ + union _rspamd_storage_u { + rspamd_fstring_t *normal; + struct _rspamd_storage_shared_s { + struct rspamd_storage_shmem *name; + gint shm_fd; + } shared; + } c; + } body_buf; + + struct rspamd_cryptobox_pubkey *peer_key; + time_t date; + time_t last_modified; + unsigned port; + int type; + gint code; + enum http_method method; + gint flags; + ref_entry_t ref; +}; + +struct rspamd_keepalive_hash_key { + rspamd_inet_addr_t *addr; + gchar *host; + gboolean is_ssl; + unsigned port; + GQueue conns; +}; + +gint32 rspamd_keep_alive_key_hash(struct rspamd_keepalive_hash_key *k); + +bool rspamd_keep_alive_key_equal(struct rspamd_keepalive_hash_key *k1, + struct rspamd_keepalive_hash_key *k2); + +KHASH_INIT(rspamd_keep_alive_hash, struct rspamd_keepalive_hash_key *, + char, 0, rspamd_keep_alive_key_hash, rspamd_keep_alive_key_equal); + +struct rspamd_http_context { + struct rspamd_http_context_cfg config; + struct rspamd_keypair_cache *client_kp_cache; + struct rspamd_cryptobox_keypair *client_kp; + struct rspamd_keypair_cache *server_kp_cache; + struct upstream_ctx *ups_ctx; + struct upstream_list *http_proxies; + gpointer ssl_ctx; + gpointer ssl_ctx_noverify; + struct ev_loop *event_loop; + ev_timer client_rotate_ev; + khash_t(rspamd_keep_alive_hash) * keep_alive_hash; +}; + +#define HTTP_ERROR http_error_quark() + +GQuark http_error_quark(void); + +void rspamd_http_message_storage_cleanup(struct rspamd_http_message *msg); + +gboolean rspamd_http_message_grow_body(struct rspamd_http_message *msg, + gsize len); + +#ifdef __cplusplus +} +#endif + +#endif /* SRC_LIBUTIL_HTTP_PRIVATE_H_ */ diff --git a/src/libserver/http/http_router.c b/src/libserver/http/http_router.c new file mode 100644 index 0000000..2fdfe48 --- /dev/null +++ b/src/libserver/http/http_router.c @@ -0,0 +1,559 @@ +/*- + * Copyright 2019 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "http_router.h" +#include "http_connection.h" +#include "http_private.h" +#include "libutil/regexp.h" +#include "libutil/printf.h" +#include "libserver/logger.h" +#include "utlist.h" +#include "unix-std.h" + +enum http_magic_type { + HTTP_MAGIC_PLAIN = 0, + HTTP_MAGIC_HTML, + HTTP_MAGIC_CSS, + HTTP_MAGIC_JS, + HTTP_MAGIC_ICO, + HTTP_MAGIC_PNG, + HTTP_MAGIC_JPG +}; + +static const struct _rspamd_http_magic { + const gchar *ext; + const gchar *ct; +} http_file_types[] = { + [HTTP_MAGIC_PLAIN] = {"txt", "text/plain"}, + [HTTP_MAGIC_HTML] = {"html", "text/html"}, + [HTTP_MAGIC_CSS] = {"css", "text/css"}, + [HTTP_MAGIC_JS] = {"js", "application/javascript"}, + [HTTP_MAGIC_ICO] = {"ico", "image/x-icon"}, + [HTTP_MAGIC_PNG] = {"png", "image/png"}, + [HTTP_MAGIC_JPG] = {"jpg", "image/jpeg"}, +}; + +/* + * HTTP router functions + */ + +static void +rspamd_http_entry_free(struct rspamd_http_connection_entry *entry) +{ + if (entry != NULL) { + close(entry->conn->fd); + rspamd_http_connection_unref(entry->conn); + if (entry->rt->finish_handler) { + entry->rt->finish_handler(entry); + } + + DL_DELETE(entry->rt->conns, entry); + g_free(entry); + } +} + +static void +rspamd_http_router_error_handler(struct rspamd_http_connection *conn, + GError *err) +{ + struct rspamd_http_connection_entry *entry = conn->ud; + struct rspamd_http_message *msg; + + if (entry->is_reply) { + /* At this point we need to finish this session and close owned socket */ + if (entry->rt->error_handler != NULL) { + entry->rt->error_handler(entry, err); + } + rspamd_http_entry_free(entry); + } + else { + /* Here we can write a reply to a client */ + if (entry->rt->error_handler != NULL) { + entry->rt->error_handler(entry, err); + } + msg = rspamd_http_new_message(HTTP_RESPONSE); + msg->date = time(NULL); + msg->code = err->code; + rspamd_http_message_set_body(msg, err->message, strlen(err->message)); + rspamd_http_connection_reset(entry->conn); + rspamd_http_connection_write_message(entry->conn, + msg, + NULL, + "text/plain", + entry, + entry->rt->timeout); + entry->is_reply = TRUE; + } +} + +static const gchar * +rspamd_http_router_detect_ct(const gchar *path) +{ + const gchar *dot; + guint i; + + dot = strrchr(path, '.'); + if (dot == NULL) { + return http_file_types[HTTP_MAGIC_PLAIN].ct; + } + dot++; + + for (i = 0; i < G_N_ELEMENTS(http_file_types); i++) { + if (strcmp(http_file_types[i].ext, dot) == 0) { + return http_file_types[i].ct; + } + } + + return http_file_types[HTTP_MAGIC_PLAIN].ct; +} + +static gboolean +rspamd_http_router_is_subdir(const gchar *parent, const gchar *sub) +{ + if (parent == NULL || sub == NULL || *parent == '\0') { + return FALSE; + } + + while (*parent != '\0') { + if (*sub != *parent) { + return FALSE; + } + parent++; + sub++; + } + + parent--; + if (*parent == G_DIR_SEPARATOR) { + return TRUE; + } + + return (*sub == G_DIR_SEPARATOR || *sub == '\0'); +} + +static gboolean +rspamd_http_router_try_file(struct rspamd_http_connection_entry *entry, + rspamd_ftok_t *lookup, gboolean expand_path) +{ + struct stat st; + gint fd; + gchar filebuf[PATH_MAX], realbuf[PATH_MAX], *dir; + struct rspamd_http_message *reply_msg; + + rspamd_snprintf(filebuf, sizeof(filebuf), "%s%c%T", + entry->rt->default_fs_path, G_DIR_SEPARATOR, lookup); + + if (realpath(filebuf, realbuf) == NULL || + lstat(realbuf, &st) == -1) { + return FALSE; + } + + if (S_ISDIR(st.st_mode) && expand_path) { + /* Try to append 'index.html' to the url */ + rspamd_fstring_t *nlookup; + rspamd_ftok_t tok; + gboolean ret; + + nlookup = rspamd_fstring_sized_new(lookup->len + sizeof("index.html")); + rspamd_printf_fstring(&nlookup, "%T%c%s", lookup, G_DIR_SEPARATOR, + "index.html"); + tok.begin = nlookup->str; + tok.len = nlookup->len; + ret = rspamd_http_router_try_file(entry, &tok, FALSE); + rspamd_fstring_free(nlookup); + + return ret; + } + else if (!S_ISREG(st.st_mode)) { + return FALSE; + } + + /* We also need to ensure that file is inside the defined dir */ + rspamd_strlcpy(filebuf, realbuf, sizeof(filebuf)); + dir = dirname(filebuf); + + if (dir == NULL || + !rspamd_http_router_is_subdir(entry->rt->default_fs_path, + dir)) { + return FALSE; + } + + fd = open(realbuf, O_RDONLY); + if (fd == -1) { + return FALSE; + } + + reply_msg = rspamd_http_new_message(HTTP_RESPONSE); + reply_msg->date = time(NULL); + reply_msg->code = 200; + rspamd_http_router_insert_headers(entry->rt, reply_msg); + + if (!rspamd_http_message_set_body_from_fd(reply_msg, fd)) { + rspamd_http_message_free(reply_msg); + close(fd); + return FALSE; + } + + close(fd); + + rspamd_http_connection_reset(entry->conn); + + msg_debug("requested file %s", realbuf); + rspamd_http_connection_write_message(entry->conn, reply_msg, NULL, + rspamd_http_router_detect_ct(realbuf), entry, + entry->rt->timeout); + + return TRUE; +} + +static void +rspamd_http_router_send_error(GError *err, + struct rspamd_http_connection_entry *entry) +{ + struct rspamd_http_message *err_msg; + + err_msg = rspamd_http_new_message(HTTP_RESPONSE); + err_msg->date = time(NULL); + err_msg->code = err->code; + rspamd_http_message_set_body(err_msg, err->message, + strlen(err->message)); + entry->is_reply = TRUE; + err_msg->status = rspamd_fstring_new_init(err->message, strlen(err->message)); + rspamd_http_router_insert_headers(entry->rt, err_msg); + rspamd_http_connection_reset(entry->conn); + rspamd_http_connection_write_message(entry->conn, + err_msg, + NULL, + "text/plain", + entry, + entry->rt->timeout); +} + + +static int +rspamd_http_router_finish_handler(struct rspamd_http_connection *conn, + struct rspamd_http_message *msg) +{ + struct rspamd_http_connection_entry *entry = conn->ud; + rspamd_http_router_handler_t handler = NULL; + gpointer found; + + GError *err; + rspamd_ftok_t lookup; + const rspamd_ftok_t *encoding; + struct http_parser_url u; + guint i; + rspamd_regexp_t *re; + struct rspamd_http_connection_router *router; + gchar *pathbuf = NULL; + + G_STATIC_ASSERT(sizeof(rspamd_http_router_handler_t) == + sizeof(gpointer)); + + memset(&lookup, 0, sizeof(lookup)); + router = entry->rt; + + if (entry->is_reply) { + /* Request is finished, it is safe to free a connection */ + rspamd_http_entry_free(entry); + } + else { + if (G_UNLIKELY(msg->method != HTTP_GET && msg->method != HTTP_POST)) { + if (router->unknown_method_handler) { + return router->unknown_method_handler(entry, msg); + } + else { + err = g_error_new(HTTP_ERROR, 500, + "Invalid method"); + if (entry->rt->error_handler != NULL) { + entry->rt->error_handler(entry, err); + } + + rspamd_http_router_send_error(err, entry); + g_error_free(err); + + return 0; + } + } + + /* Search for path */ + if (msg->url != NULL && msg->url->len != 0) { + + http_parser_parse_url(msg->url->str, msg->url->len, TRUE, &u); + + if (u.field_set & (1 << UF_PATH)) { + gsize unnorm_len; + + pathbuf = g_malloc(u.field_data[UF_PATH].len); + memcpy(pathbuf, msg->url->str + u.field_data[UF_PATH].off, + u.field_data[UF_PATH].len); + lookup.begin = pathbuf; + lookup.len = u.field_data[UF_PATH].len; + + rspamd_normalize_path_inplace(pathbuf, + lookup.len, + &unnorm_len); + lookup.len = unnorm_len; + } + else { + lookup.begin = msg->url->str; + lookup.len = msg->url->len; + } + + found = g_hash_table_lookup(entry->rt->paths, &lookup); + memcpy(&handler, &found, sizeof(found)); + msg_debug("requested known path: %T", &lookup); + } + else { + err = g_error_new(HTTP_ERROR, 404, + "Empty path requested"); + if (entry->rt->error_handler != NULL) { + entry->rt->error_handler(entry, err); + } + + rspamd_http_router_send_error(err, entry); + g_error_free(err); + + return 0; + } + + entry->is_reply = TRUE; + + encoding = rspamd_http_message_find_header(msg, "Accept-Encoding"); + + if (encoding && rspamd_substring_search(encoding->begin, encoding->len, + "gzip", 4) != -1) { + entry->support_gzip = TRUE; + } + + if (handler != NULL) { + if (pathbuf) { + g_free(pathbuf); + } + + return handler(entry, msg); + } + else { + /* Try regexps */ + for (i = 0; i < router->regexps->len; i++) { + re = g_ptr_array_index(router->regexps, i); + if (rspamd_regexp_match(re, lookup.begin, lookup.len, + TRUE)) { + found = rspamd_regexp_get_ud(re); + memcpy(&handler, &found, sizeof(found)); + + if (pathbuf) { + g_free(pathbuf); + } + + return handler(entry, msg); + } + } + + /* Now try plain file */ + if (entry->rt->default_fs_path == NULL || lookup.len == 0 || + !rspamd_http_router_try_file(entry, &lookup, TRUE)) { + + err = g_error_new(HTTP_ERROR, 404, + "Not found"); + if (entry->rt->error_handler != NULL) { + entry->rt->error_handler(entry, err); + } + + msg_info("path: %T not found", &lookup); + rspamd_http_router_send_error(err, entry); + g_error_free(err); + } + } + } + + if (pathbuf) { + g_free(pathbuf); + } + + return 0; +} + +struct rspamd_http_connection_router * +rspamd_http_router_new(rspamd_http_router_error_handler_t eh, + rspamd_http_router_finish_handler_t fh, + ev_tstamp timeout, + const char *default_fs_path, + struct rspamd_http_context *ctx) +{ + struct rspamd_http_connection_router *nrouter; + struct stat st; + + nrouter = g_malloc0(sizeof(struct rspamd_http_connection_router)); + nrouter->paths = g_hash_table_new_full(rspamd_ftok_icase_hash, + rspamd_ftok_icase_equal, rspamd_fstring_mapped_ftok_free, NULL); + nrouter->regexps = g_ptr_array_new(); + nrouter->conns = NULL; + nrouter->error_handler = eh; + nrouter->finish_handler = fh; + nrouter->response_headers = g_hash_table_new_full(rspamd_strcase_hash, + rspamd_strcase_equal, g_free, g_free); + nrouter->event_loop = ctx->event_loop; + nrouter->timeout = timeout; + nrouter->default_fs_path = NULL; + + if (default_fs_path != NULL) { + if (stat(default_fs_path, &st) == -1) { + msg_err("cannot stat %s", default_fs_path); + } + else { + if (!S_ISDIR(st.st_mode)) { + msg_err("path %s is not a directory", default_fs_path); + } + else { + nrouter->default_fs_path = realpath(default_fs_path, NULL); + } + } + } + + nrouter->ctx = ctx; + + return nrouter; +} + +void rspamd_http_router_set_key(struct rspamd_http_connection_router *router, + struct rspamd_cryptobox_keypair *key) +{ + g_assert(key != NULL); + + router->key = rspamd_keypair_ref(key); +} + +void rspamd_http_router_add_path(struct rspamd_http_connection_router *router, + const gchar *path, rspamd_http_router_handler_t handler) +{ + gpointer ptr; + rspamd_ftok_t *key; + rspamd_fstring_t *storage; + G_STATIC_ASSERT(sizeof(rspamd_http_router_handler_t) == + sizeof(gpointer)); + + if (path != NULL && handler != NULL && router != NULL) { + memcpy(&ptr, &handler, sizeof(ptr)); + storage = rspamd_fstring_new_init(path, strlen(path)); + key = g_malloc0(sizeof(*key)); + key->begin = storage->str; + key->len = storage->len; + g_hash_table_insert(router->paths, key, ptr); + } +} + +void rspamd_http_router_set_unknown_handler(struct rspamd_http_connection_router *router, + rspamd_http_router_handler_t handler) +{ + if (router != NULL) { + router->unknown_method_handler = handler; + } +} + +void rspamd_http_router_add_header(struct rspamd_http_connection_router *router, + const gchar *name, const gchar *value) +{ + if (name != NULL && value != NULL && router != NULL) { + g_hash_table_replace(router->response_headers, g_strdup(name), + g_strdup(value)); + } +} + +void rspamd_http_router_insert_headers(struct rspamd_http_connection_router *router, + struct rspamd_http_message *msg) +{ + GHashTableIter it; + gpointer k, v; + + if (router && msg) { + g_hash_table_iter_init(&it, router->response_headers); + + while (g_hash_table_iter_next(&it, &k, &v)) { + rspamd_http_message_add_header(msg, k, v); + } + } +} + +void rspamd_http_router_add_regexp(struct rspamd_http_connection_router *router, + struct rspamd_regexp_s *re, rspamd_http_router_handler_t handler) +{ + gpointer ptr; + G_STATIC_ASSERT(sizeof(rspamd_http_router_handler_t) == + sizeof(gpointer)); + + if (re != NULL && handler != NULL && router != NULL) { + memcpy(&ptr, &handler, sizeof(ptr)); + rspamd_regexp_set_ud(re, ptr); + g_ptr_array_add(router->regexps, rspamd_regexp_ref(re)); + } +} + +void rspamd_http_router_handle_socket(struct rspamd_http_connection_router *router, + gint fd, gpointer ud) +{ + struct rspamd_http_connection_entry *conn; + + conn = g_malloc0(sizeof(struct rspamd_http_connection_entry)); + conn->rt = router; + conn->ud = ud; + conn->is_reply = FALSE; + + conn->conn = rspamd_http_connection_new_server(router->ctx, + fd, + NULL, + rspamd_http_router_error_handler, + rspamd_http_router_finish_handler, + 0); + + if (router->key) { + rspamd_http_connection_set_key(conn->conn, router->key); + } + + rspamd_http_connection_read_message(conn->conn, conn, router->timeout); + DL_PREPEND(router->conns, conn); +} + +void rspamd_http_router_free(struct rspamd_http_connection_router *router) +{ + struct rspamd_http_connection_entry *conn, *tmp; + rspamd_regexp_t *re; + guint i; + + if (router) { + DL_FOREACH_SAFE(router->conns, conn, tmp) + { + rspamd_http_entry_free(conn); + } + + if (router->key) { + rspamd_keypair_unref(router->key); + } + + if (router->default_fs_path != NULL) { + g_free(router->default_fs_path); + } + + for (i = 0; i < router->regexps->len; i++) { + re = g_ptr_array_index(router->regexps, i); + rspamd_regexp_unref(re); + } + + g_ptr_array_free(router->regexps, TRUE); + g_hash_table_unref(router->paths); + g_hash_table_unref(router->response_headers); + g_free(router); + } +} diff --git a/src/libserver/http/http_router.h b/src/libserver/http/http_router.h new file mode 100644 index 0000000..1bf70ed --- /dev/null +++ b/src/libserver/http/http_router.h @@ -0,0 +1,149 @@ +/*- + * Copyright 2019 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef RSPAMD_HTTP_ROUTER_H +#define RSPAMD_HTTP_ROUTER_H + +#include "config.h" +#include "http_connection.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct rspamd_http_connection_router; +struct rspamd_http_connection_entry; + +typedef int (*rspamd_http_router_handler_t)(struct rspamd_http_connection_entry + *conn_ent, + struct rspamd_http_message *msg); + +typedef void (*rspamd_http_router_error_handler_t)(struct rspamd_http_connection_entry *conn_ent, + GError *err); + +typedef void (*rspamd_http_router_finish_handler_t)(struct rspamd_http_connection_entry *conn_ent); + + +struct rspamd_http_connection_entry { + struct rspamd_http_connection_router *rt; + struct rspamd_http_connection *conn; + gpointer ud; + gboolean is_reply; + gboolean support_gzip; + struct rspamd_http_connection_entry *prev, *next; +}; + +struct rspamd_http_connection_router { + struct rspamd_http_connection_entry *conns; + GHashTable *paths; + GHashTable *response_headers; + GPtrArray *regexps; + ev_tstamp timeout; + struct ev_loop *event_loop; + struct rspamd_http_context *ctx; + gchar *default_fs_path; + rspamd_http_router_handler_t unknown_method_handler; + struct rspamd_cryptobox_keypair *key; + rspamd_http_router_error_handler_t error_handler; + rspamd_http_router_finish_handler_t finish_handler; +}; + +/** + * Create new http connection router and the associated HTTP connection + * @param eh error handler callback + * @param fh finish handler callback + * @param default_fs_path if not NULL try to serve static files from + * the specified directory + * @return + */ +struct rspamd_http_connection_router *rspamd_http_router_new( + rspamd_http_router_error_handler_t eh, + rspamd_http_router_finish_handler_t fh, + ev_tstamp timeout, + const char *default_fs_path, + struct rspamd_http_context *ctx); + +/** + * Set encryption key for the HTTP router + * @param router router structure + * @param key opaque key structure + */ +void rspamd_http_router_set_key(struct rspamd_http_connection_router *router, + struct rspamd_cryptobox_keypair *key); + +/** + * Add new path to the router + */ +void rspamd_http_router_add_path(struct rspamd_http_connection_router *router, + const gchar *path, rspamd_http_router_handler_t handler); + +/** + * Add custom header to append to router replies + * @param router + * @param name + * @param value + */ +void rspamd_http_router_add_header(struct rspamd_http_connection_router *router, + const gchar *name, const gchar *value); + +/** + * Sets method to handle unknown request methods + * @param router + * @param handler + */ +void rspamd_http_router_set_unknown_handler(struct rspamd_http_connection_router *router, + rspamd_http_router_handler_t handler); + +/** + * Inserts router headers to the outbound message + * @param router + * @param msg + */ +void rspamd_http_router_insert_headers(struct rspamd_http_connection_router *router, + struct rspamd_http_message *msg); + +struct rspamd_regexp_s; + +/** + * Adds new pattern to router, regexp object is refcounted by this function + * @param router + * @param re + * @param handler + */ +void rspamd_http_router_add_regexp(struct rspamd_http_connection_router *router, + struct rspamd_regexp_s *re, rspamd_http_router_handler_t handler); + +/** + * Handle new accepted socket + * @param router router object + * @param fd server socket + * @param ud opaque userdata + */ +void rspamd_http_router_handle_socket( + struct rspamd_http_connection_router *router, + gint fd, + gpointer ud); + +/** + * Free router and all connections associated + * @param router + */ +void rspamd_http_router_free(struct rspamd_http_connection_router *router); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/libserver/http/http_util.c b/src/libserver/http/http_util.c new file mode 100644 index 0000000..d5c4a57 --- /dev/null +++ b/src/libserver/http/http_util.c @@ -0,0 +1,295 @@ +/*- + * Copyright 2019 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "libserver/http/http_util.h" +#include "libutil/printf.h" +#include "libutil/util.h" + +static const gchar *http_week[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +static const gchar *http_month[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + +/* + * Obtained from nginx + * Copyright (C) Igor Sysoev + */ +static guint mday[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + +time_t +rspamd_http_parse_date(const gchar *header, gsize len) +{ + const gchar *p, *end; + gint month; + guint day, year, hour, min, sec; + guint64 time; + enum { + no = 0, + rfc822, /* Tue, 10 Nov 2002 23:50:13 */ + rfc850, /* Tuesday, 10-Dec-02 23:50:13 */ + isoc /* Tue Dec 10 23:50:13 2002 */ + } fmt; + + fmt = 0; + if (len > 0) { + end = header + len; + } + else { + end = header + strlen(header); + } + + day = 32; + year = 2038; + + for (p = header; p < end; p++) { + if (*p == ',') { + break; + } + + if (*p == ' ') { + fmt = isoc; + break; + } + } + + for (p++; p < end; p++) + if (*p != ' ') { + break; + } + + if (end - p < 18) { + return (time_t) -1; + } + + if (fmt != isoc) { + if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9') { + return (time_t) -1; + } + + day = (*p - '0') * 10 + *(p + 1) - '0'; + p += 2; + + if (*p == ' ') { + if (end - p < 18) { + return (time_t) -1; + } + fmt = rfc822; + } + else if (*p == '-') { + fmt = rfc850; + } + else { + return (time_t) -1; + } + + p++; + } + + switch (*p) { + + case 'J': + month = *(p + 1) == 'a' ? 0 : *(p + 2) == 'n' ? 5 + : 6; + break; + + case 'F': + month = 1; + break; + + case 'M': + month = *(p + 2) == 'r' ? 2 : 4; + break; + + case 'A': + month = *(p + 1) == 'p' ? 3 : 7; + break; + + case 'S': + month = 8; + break; + + case 'O': + month = 9; + break; + + case 'N': + month = 10; + break; + + case 'D': + month = 11; + break; + + default: + return (time_t) -1; + } + + p += 3; + + if ((fmt == rfc822 && *p != ' ') || (fmt == rfc850 && *p != '-')) { + return (time_t) -1; + } + + p++; + + if (fmt == rfc822) { + if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9' || *(p + 2) < '0' || *(p + 2) > '9' || *(p + 3) < '0' || *(p + 3) > '9') { + return (time_t) -1; + } + + year = (*p - '0') * 1000 + (*(p + 1) - '0') * 100 + (*(p + 2) - '0') * 10 + *(p + 3) - '0'; + p += 4; + } + else if (fmt == rfc850) { + if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9') { + return (time_t) -1; + } + + year = (*p - '0') * 10 + *(p + 1) - '0'; + year += (year < 70) ? 2000 : 1900; + p += 2; + } + + if (fmt == isoc) { + if (*p == ' ') { + p++; + } + + if (*p < '0' || *p > '9') { + return (time_t) -1; + } + + day = *p++ - '0'; + + if (*p != ' ') { + if (*p < '0' || *p > '9') { + return (time_t) -1; + } + + day = day * 10 + *p++ - '0'; + } + + if (end - p < 14) { + return (time_t) -1; + } + } + + if (*p++ != ' ') { + return (time_t) -1; + } + + if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9') { + return (time_t) -1; + } + + hour = (*p - '0') * 10 + *(p + 1) - '0'; + p += 2; + + if (*p++ != ':') { + return (time_t) -1; + } + + if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9') { + return (time_t) -1; + } + + min = (*p - '0') * 10 + *(p + 1) - '0'; + p += 2; + + if (*p++ != ':') { + return (time_t) -1; + } + + if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9') { + return (time_t) -1; + } + + sec = (*p - '0') * 10 + *(p + 1) - '0'; + + if (fmt == isoc) { + p += 2; + + if (*p++ != ' ') { + return (time_t) -1; + } + + if (*p < '0' || *p > '9' || *(p + 1) < '0' || *(p + 1) > '9' || *(p + 2) < '0' || *(p + 2) > '9' || *(p + 3) < '0' || *(p + 3) > '9') { + return (time_t) -1; + } + + year = (*p - '0') * 1000 + (*(p + 1) - '0') * 100 + (*(p + 2) - '0') * 10 + *(p + 3) - '0'; + } + + if (hour > 23 || min > 59 || sec > 59) { + return (time_t) -1; + } + + if (day == 29 && month == 1) { + if ((year & 3) || ((year % 100 == 0) && (year % 400) != 0)) { + return (time_t) -1; + } + } + else if (day > mday[month]) { + return (time_t) -1; + } + + /* + * shift new year to March 1 and start months from 1 (not 0), + * it is needed for Gauss' formula + */ + + if (--month <= 0) { + month += 12; + year -= 1; + } + + /* Gauss' formula for Gregorian days since March 1, 1 BC */ + + time = (guint64) ( + /* days in years including leap years since March 1, 1 BC */ + + 365 * year + year / 4 - year / 100 + year / 400 + + /* days before the month */ + + + 367 * month / 12 - 30 + + /* days before the day */ + + + day - 1 + + /* + * 719527 days were between March 1, 1 BC and March 1, 1970, + * 31 and 28 days were in January and February 1970 + */ + + - 719527 + 31 + 28) * + 86400 + + hour * 3600 + min * 60 + sec; + + return (time_t) time; +} + +glong rspamd_http_date_format(gchar *buf, gsize len, time_t time) +{ + struct tm tms; + + rspamd_gmtime(time, &tms); + + return rspamd_snprintf(buf, len, "%s, %02d %s %4d %02d:%02d:%02d GMT", + http_week[tms.tm_wday], tms.tm_mday, + http_month[tms.tm_mon], tms.tm_year + 1900, + tms.tm_hour, tms.tm_min, tms.tm_sec); +}
\ No newline at end of file diff --git a/src/libserver/http/http_util.h b/src/libserver/http/http_util.h new file mode 100644 index 0000000..ec57508 --- /dev/null +++ b/src/libserver/http/http_util.h @@ -0,0 +1,47 @@ +/*- + * Copyright 2019 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RSPAMD_HTTP_UTIL_H +#define RSPAMD_HTTP_UTIL_H + +#include "config.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Parse HTTP date header and return it as time_t + * @param header HTTP date header + * @param len length of header + * @return time_t or (time_t)-1 in case of error + */ +time_t rspamd_http_parse_date(const gchar *header, gsize len); + +/** + * Prints HTTP date from `time` to `buf` using standard HTTP date format + * @param buf date buffer + * @param len length of buffer + * @param time time in unix seconds + * @return number of bytes written + */ +glong rspamd_http_date_format(gchar *buf, gsize len, time_t time); + +#ifdef __cplusplus +} +#endif + +#endif |