/* * 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 "config.h" #include "url.h" #include "util.h" #include "rspamd.h" #include "message.h" #include "multipattern.h" #include "contrib/uthash/utlist.h" #include "contrib/http-parser/http_parser.h" #include #include #include #include typedef struct url_match_s { const gchar *m_begin; gsize m_len; const gchar *pattern; const gchar *prefix; const gchar *newline_pos; const gchar *prev_newline_pos; gboolean add_prefix; gchar st; } url_match_t; #define URL_MATCHER_FLAG_NOHTML (1u << 0u) #define URL_MATCHER_FLAG_TLD_MATCH (1u << 1u) #define URL_MATCHER_FLAG_STAR_MATCH (1u << 2u) #define URL_MATCHER_FLAG_REGEXP (1u << 3u) struct url_callback_data; static const struct { enum rspamd_url_protocol proto; const gchar *name; gsize len; } rspamd_url_protocols[] = { {.proto = PROTOCOL_FILE, .name = "file", .len = 4}, {.proto = PROTOCOL_FTP, .name = "ftp", .len = 3}, {.proto = PROTOCOL_HTTP, .name = "http", .len = 4}, {.proto = PROTOCOL_HTTPS, .name = "https", .len = 5}, {.proto = PROTOCOL_MAILTO, .name = "mailto", .len = 6}, {.proto = PROTOCOL_TELEPHONE, .name = "tel", .len = 3}, {.proto = PROTOCOL_TELEPHONE, .name = "callto", .len = 3}, {.proto = PROTOCOL_UNKNOWN, .name = NULL, .len = 0}}; struct url_matcher { const gchar *pattern; const gchar *prefix; gboolean (*start)(struct url_callback_data *cb, const gchar *pos, url_match_t *match); gboolean (*end)(struct url_callback_data *cb, const gchar *pos, url_match_t *match); gint flags; }; static gboolean url_file_start(struct url_callback_data *cb, const gchar *pos, url_match_t *match); static gboolean url_file_end(struct url_callback_data *cb, const gchar *pos, url_match_t *match); static gboolean url_web_start(struct url_callback_data *cb, const gchar *pos, url_match_t *match); static gboolean url_web_end(struct url_callback_data *cb, const gchar *pos, url_match_t *match); static gboolean url_tld_start(struct url_callback_data *cb, const gchar *pos, url_match_t *match); static gboolean url_tld_end(struct url_callback_data *cb, const gchar *pos, url_match_t *match); static gboolean url_email_start(struct url_callback_data *cb, const gchar *pos, url_match_t *match); static gboolean url_email_end(struct url_callback_data *cb, const gchar *pos, url_match_t *match); static gboolean url_tel_start(struct url_callback_data *cb, const gchar *pos, url_match_t *match); static gboolean url_tel_end(struct url_callback_data *cb, const gchar *pos, url_match_t *match); struct url_matcher static_matchers[] = { /* Common prefixes */ {"file://", "", url_file_start, url_file_end, 0}, {"file:\\\\", "", url_file_start, url_file_end, 0}, {"ftp://", "", url_web_start, url_web_end, 0}, {"ftp:\\\\", "", url_web_start, url_web_end, 0}, {"sftp://", "", url_web_start, url_web_end, 0}, {"http:", "", url_web_start, url_web_end, 0}, {"https:", "", url_web_start, url_web_end, 0}, {"news://", "", url_web_start, url_web_end, 0}, {"nntp://", "", url_web_start, url_web_end, 0}, {"telnet://", "", url_web_start, url_web_end, 0}, {"tel:", "", url_tel_start, url_tel_end, 0}, {"webcal://", "", url_web_start, url_web_end, 0}, {"mailto:", "", url_email_start, url_email_end, 0}, {"callto:", "", url_tel_start, url_tel_end, 0}, {"h323:", "", url_web_start, url_web_end, 0}, {"sip:", "", url_web_start, url_web_end, 0}, {"www\\.[0-9a-z]", "http://", url_web_start, url_web_end, URL_MATCHER_FLAG_REGEXP}, {"ftp.", "ftp://", url_web_start, url_web_end, 0}, /* Likely emails */ { "@", "mailto://", url_email_start, url_email_end, 0}}; struct rspamd_url_flag_name { const gchar *name; gint flag; gint hash; } url_flag_names[] = { {"phished", RSPAMD_URL_FLAG_PHISHED, -1}, {"numeric", RSPAMD_URL_FLAG_NUMERIC, -1}, {"obscured", RSPAMD_URL_FLAG_OBSCURED, -1}, {"redirected", RSPAMD_URL_FLAG_REDIRECTED, -1}, {"html_displayed", RSPAMD_URL_FLAG_HTML_DISPLAYED, -1}, {"text", RSPAMD_URL_FLAG_FROM_TEXT, -1}, {"subject", RSPAMD_URL_FLAG_SUBJECT, -1}, {"host_encoded", RSPAMD_URL_FLAG_HOSTENCODED, -1}, {"schema_encoded", RSPAMD_URL_FLAG_SCHEMAENCODED, -1}, {"path_encoded", RSPAMD_URL_FLAG_PATHENCODED, -1}, {"query_encoded", RSPAMD_URL_FLAG_QUERYENCODED, -1}, {"missing_slashes", RSPAMD_URL_FLAG_MISSINGSLASHES, -1}, {"idn", RSPAMD_URL_FLAG_IDN, -1}, {"has_port", RSPAMD_URL_FLAG_HAS_PORT, -1}, {"has_user", RSPAMD_URL_FLAG_HAS_USER, -1}, {"schemaless", RSPAMD_URL_FLAG_SCHEMALESS, -1}, {"unnormalised", RSPAMD_URL_FLAG_UNNORMALISED, -1}, {"zw_spaces", RSPAMD_URL_FLAG_ZW_SPACES, -1}, {"url_displayed", RSPAMD_URL_FLAG_DISPLAY_URL, -1}, {"image", RSPAMD_URL_FLAG_IMAGE, -1}, {"query", RSPAMD_URL_FLAG_QUERY, -1}, {"content", RSPAMD_URL_FLAG_CONTENT, -1}, {"no_tld", RSPAMD_URL_FLAG_NO_TLD, -1}, {"truncated", RSPAMD_URL_FLAG_TRUNCATED, -1}, {"redirect_target", RSPAMD_URL_FLAG_REDIRECT_TARGET, -1}, {"invisible", RSPAMD_URL_FLAG_INVISIBLE, -1}, {"special", RSPAMD_URL_FLAG_SPECIAL, -1}, }; static inline khint_t rspamd_url_hash(struct rspamd_url *u); static inline khint_t rspamd_url_host_hash(struct rspamd_url *u); static inline bool rspamd_urls_cmp(struct rspamd_url *a, struct rspamd_url *b); static inline bool rspamd_urls_host_cmp(struct rspamd_url *a, struct rspamd_url *b); /* Hash table implementation */ __KHASH_IMPL(rspamd_url_hash, kh_inline, struct rspamd_url *, char, false, rspamd_url_hash, rspamd_urls_cmp); __KHASH_IMPL(rspamd_url_host_hash, kh_inline, struct rspamd_url *, char, false, rspamd_url_host_hash, rspamd_urls_host_cmp); struct url_callback_data { const gchar *begin; gchar *url_str; rspamd_mempool_t *pool; gint len; enum rspamd_url_find_type how; gboolean prefix_added; guint newline_idx; GArray *matchers; GPtrArray *newlines; const gchar *start; const gchar *fin; const gchar *end; const gchar *last_at; url_insert_function func; void *funcd; }; struct url_match_scanner { GArray *matchers_full; GArray *matchers_strict; struct rspamd_multipattern *search_trie_full; struct rspamd_multipattern *search_trie_strict; bool has_tld_file; }; struct url_match_scanner *url_scanner = NULL; enum { IS_LWSP = (1 << 0), IS_DOMAIN = (1 << 1), IS_URLSAFE = (1 << 2), IS_MAILSAFE = (1 << 3), IS_DOMAIN_END = (1 << 4) }; static const unsigned int url_scanner_table[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, IS_LWSP, IS_LWSP, IS_LWSP, IS_LWSP, IS_LWSP, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, IS_LWSP /* */, IS_MAILSAFE /* ! */, IS_URLSAFE | IS_DOMAIN_END | IS_MAILSAFE /* " */, IS_MAILSAFE /* # */, IS_MAILSAFE /* $ */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* % */, 0 /* & */, IS_MAILSAFE /* ' */, 0 /* ( */, 0 /* ) */, IS_MAILSAFE /* * */, IS_MAILSAFE /* + */, IS_MAILSAFE /* , */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* - */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* . */, IS_DOMAIN_END | IS_MAILSAFE /* / */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 0 */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 1 */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 2 */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 3 */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 4 */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 5 */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 6 */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 7 */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 8 */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* 9 */, IS_DOMAIN_END /* : */, 0 /* ; */, IS_URLSAFE | IS_DOMAIN_END /* < */, 0 /* = */, IS_URLSAFE | IS_DOMAIN_END /* > */, IS_DOMAIN_END /* ? */, 0 /* @ */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* A */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* B */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* C */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* D */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* E */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* F */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* G */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* H */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* I */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* J */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* K */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* L */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* M */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* N */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* O */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* P */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* Q */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* R */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* S */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* T */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* U */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* V */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* W */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* X */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* Y */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* Z */, 0 /* [ */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* \ */, 0 /* ] */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* ^ */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* _ */, IS_URLSAFE | IS_DOMAIN_END /* ` */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* a */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* b */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* c */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* d */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* e */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* f */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* g */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* h */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* i */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* j */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* k */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* l */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* m */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* n */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* o */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* p */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* q */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* r */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* s */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* t */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* u */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* v */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* w */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* x */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* y */, IS_URLSAFE | IS_DOMAIN | IS_MAILSAFE /* z */, IS_URLSAFE | IS_DOMAIN_END | IS_MAILSAFE /* { */, IS_URLSAFE | IS_DOMAIN_END | IS_MAILSAFE /* | */, IS_URLSAFE | IS_DOMAIN_END | IS_MAILSAFE /* } */, IS_URLSAFE | IS_DOMAIN_END | IS_MAILSAFE /* ~ */, 0, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN, IS_URLSAFE | IS_DOMAIN}; #define is_lwsp(x) ((url_scanner_table[(guchar) (x)] & IS_LWSP) != 0) #define is_mailsafe(x) ((url_scanner_table[(guchar) (x)] & (IS_MAILSAFE)) != 0) #define is_domain(x) ((url_scanner_table[(guchar) (x)] & IS_DOMAIN) != 0) #define is_urlsafe(x) ((url_scanner_table[(guchar) (x)] & (IS_URLSAFE)) != 0) const gchar * rspamd_url_strerror(int err) { switch (err) { case URI_ERRNO_OK: return "Parsing went well"; case URI_ERRNO_EMPTY: return "The URI string was empty"; case URI_ERRNO_INVALID_PROTOCOL: return "No protocol was found"; case URI_ERRNO_BAD_FORMAT: return "Bad URL format"; case URI_ERRNO_BAD_ENCODING: return "Invalid symbols encoded"; case URI_ERRNO_INVALID_PORT: return "Port number is bad"; case URI_ERRNO_TLD_MISSING: return "TLD part is not detected"; case URI_ERRNO_HOST_MISSING: return "Host part is missing"; case URI_ERRNO_TOO_LONG: return "URL is too long"; } return NULL; } static gboolean rspamd_url_parse_tld_file(const gchar *fname, struct url_match_scanner *scanner) { FILE *f; struct url_matcher m; gchar *linebuf = NULL, *p; gsize buflen = 0; gssize r; gint flags; f = fopen(fname, "r"); if (f == NULL) { msg_err("cannot open TLD file %s: %s", fname, strerror(errno)); return FALSE; } m.end = url_tld_end; m.start = url_tld_start; m.prefix = "http://"; while ((r = getline(&linebuf, &buflen, f)) > 0) { if (linebuf[0] == '/' || g_ascii_isspace(linebuf[0])) { /* Skip comment or empty line */ continue; } g_strchomp(linebuf); /* TODO: add support for ! patterns */ if (linebuf[0] == '!') { msg_debug("skip '!' patterns from parsing for now: %s", linebuf); continue; } flags = URL_MATCHER_FLAG_NOHTML | URL_MATCHER_FLAG_TLD_MATCH; if (linebuf[0] == '*') { flags |= URL_MATCHER_FLAG_STAR_MATCH; p = strchr(linebuf, '.'); if (p == NULL) { msg_err("got bad star line, skip it: %s", linebuf); continue; } p++; } else { p = linebuf; } m.flags = flags; rspamd_multipattern_add_pattern(url_scanner->search_trie_full, p, RSPAMD_MULTIPATTERN_TLD | RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8); m.pattern = rspamd_multipattern_get_pattern(url_scanner->search_trie_full, rspamd_multipattern_get_npatterns(url_scanner->search_trie_full) - 1); g_array_append_val(url_scanner->matchers_full, m); } free(linebuf); fclose(f); return TRUE; } static void rspamd_url_add_static_matchers(struct url_match_scanner *sc) { gint n = G_N_ELEMENTS(static_matchers), i; for (i = 0; i < n; i++) { if (static_matchers[i].flags & URL_MATCHER_FLAG_REGEXP) { rspamd_multipattern_add_pattern(url_scanner->search_trie_strict, static_matchers[i].pattern, RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8 | RSPAMD_MULTIPATTERN_RE); } else { rspamd_multipattern_add_pattern(url_scanner->search_trie_strict, static_matchers[i].pattern, RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8); } } g_array_append_vals(sc->matchers_strict, static_matchers, n); if (sc->matchers_full) { for (i = 0; i < n; i++) { if (static_matchers[i].flags & URL_MATCHER_FLAG_REGEXP) { rspamd_multipattern_add_pattern(url_scanner->search_trie_full, static_matchers[i].pattern, RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8 | RSPAMD_MULTIPATTERN_RE); } else { rspamd_multipattern_add_pattern(url_scanner->search_trie_full, static_matchers[i].pattern, RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8); } } g_array_append_vals(sc->matchers_full, static_matchers, n); } } void rspamd_url_deinit(void) { if (url_scanner != NULL) { if (url_scanner->search_trie_full) { rspamd_multipattern_destroy(url_scanner->search_trie_full); g_array_free(url_scanner->matchers_full, TRUE); } rspamd_multipattern_destroy(url_scanner->search_trie_strict); g_array_free(url_scanner->matchers_strict, TRUE); g_free(url_scanner); url_scanner = NULL; } } void rspamd_url_init(const gchar *tld_file) { GError *err = NULL; gboolean ret = TRUE; if (url_scanner != NULL) { rspamd_url_deinit(); } url_scanner = g_malloc(sizeof(struct url_match_scanner)); url_scanner->matchers_strict = g_array_sized_new(FALSE, TRUE, sizeof(struct url_matcher), G_N_ELEMENTS(static_matchers)); url_scanner->search_trie_strict = rspamd_multipattern_create_sized( G_N_ELEMENTS(static_matchers), RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8); if (tld_file) { /* Reserve larger multipattern */ url_scanner->matchers_full = g_array_sized_new(FALSE, TRUE, sizeof(struct url_matcher), 13000); url_scanner->search_trie_full = rspamd_multipattern_create_sized(13000, RSPAMD_MULTIPATTERN_ICASE | RSPAMD_MULTIPATTERN_UTF8); url_scanner->has_tld_file = true; } else { url_scanner->matchers_full = NULL; url_scanner->search_trie_full = NULL; url_scanner->has_tld_file = false; } rspamd_url_add_static_matchers(url_scanner); if (tld_file != NULL) { ret = rspamd_url_parse_tld_file(tld_file, url_scanner); } if (url_scanner->matchers_full && url_scanner->matchers_full->len > 1000) { msg_info("start compiling of %d TLD suffixes; it might take a long time", url_scanner->matchers_full->len); } if (!rspamd_multipattern_compile(url_scanner->search_trie_strict, &err)) { msg_err("cannot compile url matcher static patterns, fatal error: %e", err); abort(); } if (url_scanner->search_trie_full) { if (!rspamd_multipattern_compile(url_scanner->search_trie_full, &err)) { msg_err("cannot compile tld patterns, url matching will be " "incomplete: %e", err); g_error_free(err); ret = FALSE; } } if (tld_file != NULL) { if (ret) { msg_info("initialized %ud url match suffixes from '%s'", url_scanner->matchers_full->len - url_scanner->matchers_strict->len, tld_file); } else { msg_err("failed to initialize url tld suffixes from '%s', " "use %ud internal match suffixes", tld_file, url_scanner->matchers_strict->len); } } /* Generate hashes for flags */ for (gint i = 0; i < G_N_ELEMENTS(url_flag_names); i++) { url_flag_names[i].hash = rspamd_cryptobox_fast_hash_specific(RSPAMD_CRYPTOBOX_HASHFAST_INDEPENDENT, url_flag_names[i].name, strlen(url_flag_names[i].name), 0); } /* Ensure that we have no hashes collisions O(N^2) but this array is small */ for (gint i = 0; i < G_N_ELEMENTS(url_flag_names) - 1; i++) { for (gint j = i + 1; j < G_N_ELEMENTS(url_flag_names); j++) { if (url_flag_names[i].hash == url_flag_names[j].hash) { msg_err("collision: both %s and %s map to %d", url_flag_names[i].name, url_flag_names[j].name, url_flag_names[i].hash); abort(); } } } } #define SET_U(u, field) \ do { \ if ((u) != NULL) { \ (u)->field_set |= 1 << (field); \ (u)->field_data[(field)].len = p - c; \ (u)->field_data[(field)].off = c - str; \ } \ } while (0) static bool is_url_start(gchar c) { if (c == '(' || c == '{' || c == '[' || c == '<' || c == '\'') { return TRUE; } return FALSE; } static bool is_url_end(gchar c) { if (c == ')' || c == '}' || c == ']' || c == '>' || c == '\'') { return TRUE; } return FALSE; } static bool is_domain_start(int p) { if (g_ascii_isalnum(p) || p == '[' || p == '%' || p == '_' || (p & 0x80)) { return TRUE; } return FALSE; } static const guint max_domain_length = 253; static const guint max_dns_label = 63; static const guint max_email_user = 64; static gint rspamd_mailto_parse(struct http_parser_url *u, const gchar *str, gsize len, gchar const **end, enum rspamd_url_parse_flags parse_flags, guint *flags) { const gchar *p = str, *c = str, *last = str + len; gchar t; gint ret = 1; enum { parse_mailto, parse_slash, parse_slash_slash, parse_semicolon, parse_prefix_question, parse_destination, parse_equal, parse_user, parse_at, parse_domain, parse_suffix_question, parse_query } st = parse_mailto; if (u != NULL) { memset(u, 0, sizeof(*u)); } while (p < last) { t = *p; if (p - str > max_email_user + max_domain_length + 1) { goto out; } switch (st) { case parse_mailto: if (t == ':') { st = parse_semicolon; SET_U(u, UF_SCHEMA); } p++; break; case parse_semicolon: if (t == '/' || t == '\\') { st = parse_slash; p++; } else { *flags |= RSPAMD_URL_FLAG_MISSINGSLASHES; st = parse_slash_slash; } break; case parse_slash: if (t == '/' || t == '\\') { st = parse_slash_slash; } else { goto out; } p++; break; case parse_slash_slash: if (t == '?') { st = parse_prefix_question; p++; } else if (t != '/' && t != '\\') { c = p; st = parse_user; } else { /* Skip multiple slashes */ p++; } break; case parse_prefix_question: if (t == 't') { /* XXX: accept only to= */ st = parse_destination; } else { goto out; } break; case parse_destination: if (t == '=') { st = parse_equal; } p++; break; case parse_equal: c = p; st = parse_user; break; case parse_user: if (t == '@') { if (p - c == 0) { goto out; } SET_U(u, UF_USERINFO); st = parse_at; } else if (!is_mailsafe(t)) { goto out; } else if (p - c > max_email_user) { goto out; } p++; break; case parse_at: c = p; st = parse_domain; break; case parse_domain: if (t == '?') { SET_U(u, UF_HOST); st = parse_suffix_question; } else if (!is_domain(t) && t != '.' && t != '_') { goto out; } else if (p - c > max_domain_length) { goto out; } p++; break; case parse_suffix_question: c = p; st = parse_query; break; case parse_query: if (t == '#') { if (p - c != 0) { SET_U(u, UF_QUERY); } c = p + 1; ret = 0; goto out; } else if (!(parse_flags & RSPAMD_URL_PARSE_HREF) && is_url_end(t)) { ret = 0; goto out; } else if (is_lwsp(t)) { if (!(parse_flags & RSPAMD_URL_PARSE_CHECK)) { if (g_ascii_isspace(t)) { ret = 0; } goto out; } else { goto out; } } p++; break; } } if (st == parse_domain) { if (p - c != 0) { SET_U(u, UF_HOST); ret = 0; } } else if (st == parse_query) { if (p - c > 0) { SET_U(u, UF_QUERY); } ret = 0; } out: if (end != NULL) { *end = p; } if ((parse_flags & RSPAMD_URL_PARSE_CHECK)) { return 0; } return ret; } static gint rspamd_telephone_parse(struct http_parser_url *u, const gchar *str, gsize len, gchar const **end, enum rspamd_url_parse_flags parse_flags, guint *flags) { enum { parse_protocol, parse_semicolon, parse_slash, parse_slash_slash, parse_spaces, parse_plus, parse_phone_start, parse_phone, } st = parse_protocol; const gchar *p = str, *c = str, *last = str + len; gchar t; gint ret = 1, i; UChar32 uc; if (u != NULL) { memset(u, 0, sizeof(*u)); } while (p < last) { t = *p; if (p - str > max_email_user) { goto out; } switch (st) { case parse_protocol: if (t == ':') { st = parse_semicolon; SET_U(u, UF_SCHEMA); } p++; break; case parse_semicolon: if (t == '/' || t == '\\') { st = parse_slash; p++; } else { st = parse_slash_slash; } break; case parse_slash: if (t == '/' || t == '\\') { st = parse_slash_slash; } else { goto out; } p++; break; case parse_slash_slash: if (g_ascii_isspace(t)) { st = parse_spaces; p++; } else if (t == '+') { c = p; st = parse_plus; } else if (t == '/') { /* Skip multiple slashes */ p++; } else { st = parse_phone_start; c = p; } break; case parse_spaces: if (t == '+') { c = p; st = parse_plus; } else if (!g_ascii_isspace(t)) { st = parse_phone_start; c = p; } else { p++; } break; case parse_plus: c = p; p++; st = parse_phone_start; break; case parse_phone_start: if (*p == '%' || *p == '(' || g_ascii_isdigit(*p)) { st = parse_phone; p++; } else { goto out; } break; case parse_phone: i = p - str; U8_NEXT(str, i, len, uc); p = str + i; if (u_isdigit(uc) || uc == '(' || uc == ')' || uc == '[' || uc == ']' || u_isspace(uc) || uc == '%') { /* p is already incremented by U8_NEXT! */ } else if (uc <= 0 || is_url_end(uc)) { ret = 0; goto set; } break; } } set: if (st == parse_phone) { if (p - c != 0) { SET_U(u, UF_HOST); ret = 0; } } out: if (end != NULL) { *end = p; } if ((parse_flags & RSPAMD_URL_PARSE_CHECK)) { return 0; } return ret; } static gint rspamd_web_parse(struct http_parser_url *u, const gchar *str, gsize len, gchar const **end, enum rspamd_url_parse_flags parse_flags, guint *flags) { const gchar *p = str, *c = str, *last = str + len, *slash = NULL, *password_start = NULL, *user_start = NULL; gchar t = 0; UChar32 uc; glong pt; gint ret = 1; gboolean user_seen = FALSE; enum { parse_protocol, parse_slash, parse_slash_slash, parse_semicolon, parse_user, parse_at, parse_multiple_at, parse_password_start, parse_password, parse_domain_start, parse_domain, parse_ipv6, parse_port_password, parse_port, parse_suffix_slash, parse_path, parse_query, parse_part } st = parse_protocol; if (u != NULL) { memset(u, 0, sizeof(*u)); } while (p < last) { t = *p; switch (st) { case parse_protocol: if (t == ':') { st = parse_semicolon; SET_U(u, UF_SCHEMA); } else if (!g_ascii_isalnum(t) && t != '+' && t != '-') { if ((parse_flags & RSPAMD_URL_PARSE_CHECK) && p > c) { /* We might have some domain, but no protocol */ st = parse_domain_start; p = c; slash = c; break; } else { goto out; } } p++; break; case parse_semicolon: if (t == '/' || t == '\\') { st = parse_slash; p++; } else { st = parse_slash_slash; *(flags) |= RSPAMD_URL_FLAG_MISSINGSLASHES; } break; case parse_slash: if (t == '/' || t == '\\') { st = parse_slash_slash; } else { goto out; } p++; break; case parse_slash_slash: if (t != '/' && t != '\\') { c = p; slash = p; st = parse_domain_start; /* * Unfortunately, due to brain damage of the RFC 3986 authors, * we have to distinguish two possibilities here: * authority = [ userinfo "@" ] host [ ":" port ] * So if we have @ somewhere before hostname then we must process * with the username state. Otherwise, we have to process via * the hostname state. Unfortunately, there is no way to distinguish * them aside of running NFA or two DFA or performing lookahead. * Lookahead approach looks easier to implement. */ const char *tp = p; while (tp < last) { if (*tp == '@') { user_seen = TRUE; st = parse_user; break; } else if (*tp == '/' || *tp == '#' || *tp == '?') { st = parse_domain_start; break; } tp++; } if (st == parse_domain_start && *p == '[') { st = parse_ipv6; p++; c = p; } } else { /* Skip multiple slashes */ p++; } break; case parse_ipv6: if (t == ']') { if (p - c == 0) { goto out; } SET_U(u, UF_HOST); p++; if (*p == ':') { st = parse_port; c = p + 1; } else if (*p == '/' || *p == '\\') { st = parse_path; c = p + 1; } else if (*p == '?') { st = parse_query; c = p + 1; } else if (*p == '#') { st = parse_part; c = p + 1; } else if (p != last) { goto out; } } else if (!g_ascii_isxdigit(t) && t != ':' && t != '.') { goto out; } p++; break; case parse_user: if (t == ':') { if (p - c == 0) { goto out; } user_start = c; st = parse_password_start; } else if (t == '@') { /* No password */ if (p - c == 0) { /* We have multiple at in fact */ st = parse_multiple_at; user_seen = TRUE; *flags |= RSPAMD_URL_FLAG_OBSCURED; continue; } SET_U(u, UF_USERINFO); *flags |= RSPAMD_URL_FLAG_HAS_USER; st = parse_at; } else if (!g_ascii_isgraph(t)) { goto out; } else if (p - c > max_email_user) { goto out; } p++; break; case parse_multiple_at: if (t != '@') { if (p - c == 0) { goto out; } /* For now, we ignore all that stuff as it is bogus */ /* Off by one */ p--; SET_U(u, UF_USERINFO); p++; *flags |= RSPAMD_URL_FLAG_HAS_USER; st = parse_at; } else { p++; } break; case parse_password_start: if (t == '@') { /* Empty password */ SET_U(u, UF_USERINFO); if (u != NULL && u->field_data[UF_USERINFO].len > 0) { /* Eat semicolon */ u->field_data[UF_USERINFO].len--; } *flags |= RSPAMD_URL_FLAG_HAS_USER; st = parse_at; } else { c = p; password_start = p; st = parse_password; } p++; break; case parse_password: if (t == '@') { /* XXX: password is not stored */ if (u != NULL) { if (u->field_data[UF_USERINFO].len == 0 && password_start && user_start && password_start > user_start + 1) { *flags |= RSPAMD_URL_FLAG_HAS_USER; u->field_set |= 1u << (UF_USERINFO); u->field_data[UF_USERINFO].len = password_start - user_start - 1; u->field_data[UF_USERINFO].off = user_start - str; } } st = parse_at; } else if (!g_ascii_isgraph(t)) { goto out; } else if (p - c > max_domain_length) { goto out; } p++; break; case parse_at: c = p; if (t == '@') { *flags |= RSPAMD_URL_FLAG_OBSCURED; p++; } else if (t == '[') { st = parse_ipv6; p++; c = p; } else { st = parse_domain_start; } break; case parse_domain_start: if (is_domain_start(t)) { st = parse_domain; } else { goto out; } break; case parse_domain: if (p - c > max_domain_length) { /* Too large domain */ goto out; } if (t == '/' || t == '\\' || t == ':' || t == '?' || t == '#') { if (p - c == 0) { goto out; } if (t == '/' || t == '\\') { SET_U(u, UF_HOST); st = parse_suffix_slash; } else if (t == '?') { SET_U(u, UF_HOST); st = parse_query; c = p + 1; } else if (t == '#') { SET_U(u, UF_HOST); st = parse_part; c = p + 1; } else if (t == ':' && !user_seen) { /* * Here we can have both port and password, hence we need * to apply some heuristic here */ st = parse_port_password; } else { /* * We can go only for parsing port here */ SET_U(u, UF_HOST); st = parse_port; c = p + 1; } p++; } else { if (is_url_end(t) || is_url_start(t)) { goto set; } else if (*p == '@' && !user_seen) { /* We need to fallback and test user */ p = slash; user_seen = TRUE; st = parse_user; } else if (*p != '.' && *p != '-' && *p != '_' && *p != '%') { if (*p & 0x80) { guint i = 0; U8_NEXT(((const guchar *) p), i, last - p, uc); if (uc < 0) { /* Bad utf8 */ goto out; } if (!u_isalnum(uc)) { /* Bad symbol */ if (IS_ZERO_WIDTH_SPACE(uc)) { (*flags) |= RSPAMD_URL_FLAG_ZW_SPACES; } else { if (!u_isgraph(uc)) { if (!(parse_flags & RSPAMD_URL_PARSE_CHECK)) { goto out; } else { goto set; } } } } else { (*flags) |= RSPAMD_URL_FLAG_IDN; } p = p + i; } else if (is_urlsafe(*p)) { p++; } else { if (parse_flags & RSPAMD_URL_PARSE_HREF) { /* We have to use all shit we are given here */ p++; (*flags) |= RSPAMD_URL_FLAG_OBSCURED; } else { if (!(parse_flags & RSPAMD_URL_PARSE_CHECK)) { goto out; } else { goto set; } } } } else { p++; } } break; case parse_port_password: if (g_ascii_isdigit(t)) { const gchar *tmp = p; while (tmp < last) { if (!g_ascii_isdigit(*tmp)) { if (*tmp == '/' || *tmp == '#' || *tmp == '?' || is_url_end(*tmp) || g_ascii_isspace(*tmp)) { /* Port + something */ st = parse_port; c = slash; p--; SET_U(u, UF_HOST); p++; c = p; break; } else { /* Not a port, bad character at the end */ break; } } tmp++; } if (tmp == last) { /* Host + port only */ st = parse_port; c = slash; p--; SET_U(u, UF_HOST); p++; c = p; } if (st != parse_port) { /* Fallback to user:password */ p = slash; c = slash; user_seen = TRUE; st = parse_user; } } else { /* Rewind back */ p = slash; c = slash; user_seen = TRUE; st = parse_user; } break; case parse_port: if (t == '/' || t == '\\') { pt = strtoul(c, NULL, 10); if (pt == 0 || pt > 65535) { goto out; } if (u != NULL) { u->port = pt; *flags |= RSPAMD_URL_FLAG_HAS_PORT; } st = parse_suffix_slash; } else if (t == '?') { pt = strtoul(c, NULL, 10); if (pt == 0 || pt > 65535) { goto out; } if (u != NULL) { u->port = pt; *flags |= RSPAMD_URL_FLAG_HAS_PORT; } c = p + 1; st = parse_query; } else if (t == '#') { pt = strtoul(c, NULL, 10); if (pt == 0 || pt > 65535) { goto out; } if (u != NULL) { u->port = pt; *flags |= RSPAMD_URL_FLAG_HAS_PORT; } c = p + 1; st = parse_part; } else if (is_url_end(t)) { goto set; } else if (!g_ascii_isdigit(t)) { if (!(parse_flags & RSPAMD_URL_PARSE_CHECK) || !g_ascii_isspace(t)) { goto out; } else { goto set; } } p++; break; case parse_suffix_slash: if (t != '/' && t != '\\') { c = p; st = parse_path; } else { /* Skip extra slashes */ p++; } break; case parse_path: if (t == '?') { if (p - c != 0) { SET_U(u, UF_PATH); } c = p + 1; st = parse_query; } else if (t == '#') { /* No query, just fragment */ if (p - c != 0) { SET_U(u, UF_PATH); } c = p + 1; st = parse_part; } else if (!(parse_flags & RSPAMD_URL_PARSE_HREF) && is_url_end(t)) { goto set; } else if (is_lwsp(t)) { if (!(parse_flags & RSPAMD_URL_PARSE_CHECK)) { if (g_ascii_isspace(t)) { goto set; } goto out; } else { goto set; } } p++; break; case parse_query: if (t == '#') { if (p - c != 0) { SET_U(u, UF_QUERY); } c = p + 1; st = parse_part; } else if (!(parse_flags & RSPAMD_URL_PARSE_HREF) && is_url_end(t)) { goto set; } else if (is_lwsp(t)) { if (!(parse_flags & RSPAMD_URL_PARSE_CHECK)) { if (g_ascii_isspace(t)) { goto set; } goto out; } else { goto set; } } p++; break; case parse_part: if (!(parse_flags & RSPAMD_URL_PARSE_HREF) && is_url_end(t)) { goto set; } else if (is_lwsp(t)) { if (!(parse_flags & RSPAMD_URL_PARSE_CHECK)) { if (g_ascii_isspace(t)) { goto set; } goto out; } else { goto set; } } p++; break; } } set: /* Parse remaining */ switch (st) { case parse_domain: if (p - c == 0 || !is_domain(*(p - 1)) || !is_domain(*c)) { goto out; } SET_U(u, UF_HOST); ret = 0; break; case parse_port: pt = strtoul(c, NULL, 10); if (pt == 0 || pt > 65535) { goto out; } if (u != NULL) { u->port = pt; } ret = 0; break; case parse_suffix_slash: /* Url ends with '/' */ ret = 0; break; case parse_path: if (p - c > 0) { SET_U(u, UF_PATH); } ret = 0; break; case parse_query: if (p - c > 0) { SET_U(u, UF_QUERY); } ret = 0; break; case parse_part: if (p - c > 0) { SET_U(u, UF_FRAGMENT); } ret = 0; break; case parse_ipv6: if (t != ']') { ret = 1; } else { /* e.g. http://[::] */ ret = 0; } break; default: /* Error state */ ret = 1; break; } out: if (end != NULL) { *end = p; } return ret; } #undef SET_U static gint rspamd_tld_trie_callback(struct rspamd_multipattern *mp, guint strnum, gint match_start, gint match_pos, const gchar *text, gsize len, void *context) { struct url_matcher *matcher; const gchar *start, *pos, *p; struct rspamd_url *url = context; gint ndots; matcher = &g_array_index(url_scanner->matchers_full, struct url_matcher, strnum); ndots = 1; if (matcher->flags & URL_MATCHER_FLAG_STAR_MATCH) { /* Skip one more tld component */ ndots++; } pos = text + match_start; p = pos - 1; start = rspamd_url_host_unsafe(url); if (*pos != '.' || match_pos != (gint) url->hostlen) { /* Something weird has been found */ if (match_pos == (gint) url->hostlen - 1) { pos = rspamd_url_host_unsafe(url) + match_pos; if (*pos == '.') { /* This is dot at the end of domain */ url->hostlen--; } else { return 0; } } else { return 0; } } /* Now we need to find top level domain */ pos = start; while (p >= start && ndots > 0) { if (*p == '.') { ndots--; pos = p + 1; } else { pos = p; } p--; } if ((ndots == 0 || p == start - 1) && url->tldlen < rspamd_url_host_unsafe(url) + url->hostlen - pos) { url->tldshift = (pos - url->string); url->tldlen = rspamd_url_host_unsafe(url) + url->hostlen - pos; } return 0; } static void rspamd_url_regen_from_inet_addr(struct rspamd_url *uri, const void *addr, int af, rspamd_mempool_t *pool) { gchar *strbuf, *p; const gchar *start_offset; gsize slen = uri->urllen - uri->hostlen; goffset r = 0; if (af == AF_INET) { slen += INET_ADDRSTRLEN; } else { slen += INET6_ADDRSTRLEN; } if (uri->flags & RSPAMD_URL_FLAG_HAS_PORT) { slen += sizeof("65535") - 1; } /* Allocate new string to build it from IP */ strbuf = rspamd_mempool_alloc(pool, slen + 1); r += rspamd_snprintf(strbuf + r, slen - r, "%*s", (gint) (uri->hostshift), uri->string); uri->hostshift = r; uri->tldshift = r; start_offset = strbuf + r; inet_ntop(af, addr, strbuf + r, slen - r + 1); uri->hostlen = strlen(start_offset); r += uri->hostlen; uri->tldlen = uri->hostlen; uri->flags |= RSPAMD_URL_FLAG_NUMERIC; /* Reconstruct URL */ if (uri->flags & RSPAMD_URL_FLAG_HAS_PORT && uri->ext) { p = strbuf + r; start_offset = p + 1; r += rspamd_snprintf(strbuf + r, slen - r, ":%ud", (unsigned int) uri->ext->port); } if (uri->datalen > 0) { p = strbuf + r; start_offset = p + 1; r += rspamd_snprintf(strbuf + r, slen - r, "/%*s", (gint) uri->datalen, rspamd_url_data_unsafe(uri)); uri->datashift = start_offset - strbuf; } else { /* Add trailing slash if needed */ if (uri->hostlen + uri->hostshift < uri->urllen && *(rspamd_url_host_unsafe(uri) + uri->hostlen) == '/') { r += rspamd_snprintf(strbuf + r, slen - r, "/"); } } if (uri->querylen > 0) { p = strbuf + r; start_offset = p + 1; r += rspamd_snprintf(strbuf + r, slen - r, "?%*s", (gint) uri->querylen, rspamd_url_query_unsafe(uri)); uri->queryshift = start_offset - strbuf; } if (uri->fragmentlen > 0) { p = strbuf + r; start_offset = p + 1; r += rspamd_snprintf(strbuf + r, slen - r, "#%*s", (gint) uri->fragmentlen, rspamd_url_fragment_unsafe(uri)); uri->fragmentshift = start_offset - strbuf; } uri->string = strbuf; uri->urllen = r; } static gboolean rspamd_url_maybe_regenerate_from_ip(struct rspamd_url *uri, rspamd_mempool_t *pool) { const gchar *p, *end, *c; gchar *errstr; struct in_addr in4; struct in6_addr in6; gboolean ret = FALSE, check_num = TRUE; guint32 n, dots, t = 0, i = 0, shift, nshift; p = rspamd_url_host_unsafe(uri); end = p + uri->hostlen; if (*p == '[' && *(end - 1) == ']') { p++; end--; } while (*(end - 1) == '.' && end > p) { end--; } if (end - p == 0 || end - p > INET6_ADDRSTRLEN) { return FALSE; } if (rspamd_str_has_8bit(p, end - p)) { return FALSE; } if (rspamd_parse_inet_address_ip4(p, end - p, &in4)) { rspamd_url_regen_from_inet_addr(uri, &in4, AF_INET, pool); ret = TRUE; } else if (rspamd_parse_inet_address_ip6(p, end - p, &in6)) { rspamd_url_regen_from_inet_addr(uri, &in6, AF_INET6, pool); ret = TRUE; } else { /* Heuristics for broken urls */ gchar buf[INET6_ADDRSTRLEN + 1]; /* Try also numeric notation */ c = p; n = 0; dots = 0; shift = 0; while (p <= end && check_num) { if (shift < 32 && ((*p == '.' && dots < 3) || (p == end && dots <= 3))) { if (p - c + 1 >= (gint) sizeof(buf)) { msg_debug_pool("invalid numeric url %*.s...: too long", INET6_ADDRSTRLEN, c); return FALSE; } rspamd_strlcpy(buf, c, p - c + 1); c = p + 1; if (p < end && *p == '.') { dots++; } glong long_n = strtol(buf, &errstr, 0); if ((errstr == NULL || *errstr == '\0') && long_n >= 0) { t = long_n; /* Truncate as windows does */ /* * Even if we have zero, we need to shift by 1 octet */ nshift = (t == 0 ? shift + 8 : shift); /* * Here we count number of octets encoded in this element */ for (i = 0; i < 4; i++) { if ((t >> (8 * i)) > 0) { nshift += 8; } else { break; } } /* * Here we need to find the proper shift of the previous * components, so we check possible cases: * 1) 1 octet - just use it applying shift * 2) 2 octets - convert to big endian 16 bit number * 3) 3 octets - convert to big endian 24 bit number * 4) 4 octets - convert to big endian 32 bit number */ switch (i) { case 4: t = GUINT32_TO_BE(t); break; case 3: t = (GUINT32_TO_BE(t & 0xFFFFFFU)) >> 8; break; case 2: t = GUINT16_TO_BE(t & 0xFFFFU); break; default: t = t & 0xFF; break; } if (p != end) { n |= t << shift; shift = nshift; } } else { check_num = FALSE; } } p++; } /* The last component should be last according to url normalization: * 192.168.1 -> 192.168.0.1 * 192 -> 0.0.0.192 * 192.168 -> 192.0.0.168 */ shift = 8 * (4 - i); if (shift < 32) { n |= t << shift; } if (check_num) { if (dots <= 4) { memcpy(&in4, &n, sizeof(in4)); rspamd_url_regen_from_inet_addr(uri, &in4, AF_INET, pool); uri->flags |= RSPAMD_URL_FLAG_OBSCURED; ret = TRUE; } else if (end - c > (gint) sizeof(buf) - 1) { rspamd_strlcpy(buf, c, end - c + 1); if (inet_pton(AF_INET6, buf, &in6) == 1) { rspamd_url_regen_from_inet_addr(uri, &in6, AF_INET6, pool); uri->flags |= RSPAMD_URL_FLAG_OBSCURED; ret = TRUE; } } } } return ret; } static void rspamd_url_shift(struct rspamd_url *uri, gsize nlen, enum http_parser_url_fields field) { guint old_shift, shift = 0; gint remain; /* Shift remaining data */ switch (field) { case UF_SCHEMA: if (nlen >= uri->protocollen) { return; } else { shift = uri->protocollen - nlen; } old_shift = uri->protocollen; uri->protocollen -= shift; remain = uri->urllen - uri->protocollen; g_assert(remain >= 0); memmove(uri->string + uri->protocollen, uri->string + old_shift, remain); uri->urllen -= shift; uri->flags |= RSPAMD_URL_FLAG_SCHEMAENCODED; break; case UF_HOST: if (nlen >= uri->hostlen) { return; } else { shift = uri->hostlen - nlen; } old_shift = uri->hostlen; uri->hostlen -= shift; remain = (uri->urllen - (uri->hostshift)) - old_shift; g_assert(remain >= 0); memmove(rspamd_url_host_unsafe(uri) + uri->hostlen, rspamd_url_host_unsafe(uri) + old_shift, remain); uri->urllen -= shift; uri->flags |= RSPAMD_URL_FLAG_HOSTENCODED; break; case UF_PATH: if (nlen >= uri->datalen) { return; } else { shift = uri->datalen - nlen; } old_shift = uri->datalen; uri->datalen -= shift; remain = (uri->urllen - (uri->datashift)) - old_shift; g_assert(remain >= 0); memmove(rspamd_url_data_unsafe(uri) + uri->datalen, rspamd_url_data_unsafe(uri) + old_shift, remain); uri->urllen -= shift; uri->flags |= RSPAMD_URL_FLAG_PATHENCODED; break; case UF_QUERY: if (nlen >= uri->querylen) { return; } else { shift = uri->querylen - nlen; } old_shift = uri->querylen; uri->querylen -= shift; remain = (uri->urllen - (uri->queryshift)) - old_shift; g_assert(remain >= 0); memmove(rspamd_url_query_unsafe(uri) + uri->querylen, rspamd_url_query_unsafe(uri) + old_shift, remain); uri->urllen -= shift; uri->flags |= RSPAMD_URL_FLAG_QUERYENCODED; break; case UF_FRAGMENT: if (nlen >= uri->fragmentlen) { return; } else { shift = uri->fragmentlen - nlen; } uri->fragmentlen -= shift; uri->urllen -= shift; break; default: break; } /* Now adjust lengths and offsets */ switch (field) { case UF_SCHEMA: if (uri->userlen > 0) { uri->usershift -= shift; } if (uri->hostlen > 0) { uri->hostshift -= shift; } /* Go forward */ /* FALLTHRU */ case UF_HOST: if (uri->datalen > 0) { uri->datashift -= shift; } /* Go forward */ /* FALLTHRU */ case UF_PATH: if (uri->querylen > 0) { uri->queryshift -= shift; } /* Go forward */ /* FALLTHRU */ case UF_QUERY: if (uri->fragmentlen > 0) { uri->fragmentshift -= shift; } /* Go forward */ /* FALLTHRU */ case UF_FRAGMENT: default: break; } } static void rspamd_telephone_normalise_inplace(struct rspamd_url *uri) { gchar *t, *h, *end; gint i = 0, w, orig_len; UChar32 uc; t = rspamd_url_host_unsafe(uri); h = t; end = t + uri->hostlen; orig_len = uri->hostlen; if (*h == '+') { h++; t++; } while (h < end) { i = 0; U8_NEXT(h, i, end - h, uc); if (u_isdigit(uc)) { w = 0; U8_APPEND_UNSAFE(t, w, uc); t += w; } h += i; } uri->hostlen = t - rspamd_url_host_unsafe(uri); uri->urllen -= (orig_len - uri->hostlen); } static inline bool is_idna_label_dot(UChar ch) { switch (ch) { case 0x3002: case 0xFF0E: case 0xFF61: return true; default: return false; } } /* * All credits for this investigation should go to * Dr. Hajime Shimada and Mr. Shirakura as they have revealed this case in their * research. */ /* * This function replaces unsafe IDNA dots in host labels. Unfortunately, * IDNA extends dot definition from '.' to multiple other characters that * should be treated equally. * This function replaces such dots and returns `true` if these dots are found. * In this case, it should be treated as obfuscation attempt. */ static bool rspamd_url_remove_dots(struct rspamd_url *uri) { const gchar *hstart = rspamd_url_host_unsafe(uri); gchar *t; UChar32 uc; gint i = 0, hlen; bool ret = false; if (uri->hostlen == 0) { return false; } hlen = uri->hostlen; t = rspamd_url_host_unsafe(uri); while (i < hlen) { gint prev_i = i; U8_NEXT(hstart, i, hlen, uc); if (is_idna_label_dot(uc)) { *t++ = '.'; ret = true; } else { if (ret) { /* We have to shift the remaining stuff */ while (prev_i < i) { *t++ = *(hstart + prev_i); prev_i++; } } else { t += (i - prev_i); } } } if (ret) { rspamd_url_shift(uri, t - hstart, UF_HOST); } return ret; } enum uri_errno rspamd_url_parse(struct rspamd_url *uri, gchar *uristring, gsize len, rspamd_mempool_t *pool, enum rspamd_url_parse_flags parse_flags) { struct http_parser_url u; gchar *p; const gchar *end; guint complen, ret, flags = 0; gsize unquoted_len = 0; memset(uri, 0, sizeof(*uri)); memset(&u, 0, sizeof(u)); uri->count = 1; /* Undefine order */ uri->order = -1; uri->part_order = -1; if (*uristring == '\0') { return URI_ERRNO_EMPTY; } if (len >= G_MAXUINT16 / 2) { flags |= RSPAMD_URL_FLAG_TRUNCATED; len = G_MAXUINT16 / 2; } p = uristring; uri->protocol = PROTOCOL_UNKNOWN; if (len > sizeof("mailto:") - 1) { /* For mailto: urls we also need to add slashes to make it a valid URL */ if (g_ascii_strncasecmp(p, "mailto:", sizeof("mailto:") - 1) == 0) { ret = rspamd_mailto_parse(&u, uristring, len, &end, parse_flags, &flags); } else if (g_ascii_strncasecmp(p, "tel:", sizeof("tel:") - 1) == 0 || g_ascii_strncasecmp(p, "callto:", sizeof("callto:") - 1) == 0) { ret = rspamd_telephone_parse(&u, uristring, len, &end, parse_flags, &flags); uri->protocol = PROTOCOL_TELEPHONE; } else { ret = rspamd_web_parse(&u, uristring, len, &end, parse_flags, &flags); } } else { ret = rspamd_web_parse(&u, uristring, len, &end, parse_flags, &flags); } if (ret != 0) { return URI_ERRNO_BAD_FORMAT; } if (end > uristring && (guint) (end - uristring) != len) { len = end - uristring; } uri->raw = p; uri->rawlen = len; if (flags & RSPAMD_URL_FLAG_MISSINGSLASHES) { len += 2; uri->string = rspamd_mempool_alloc(pool, len + 1); memcpy(uri->string, p, u.field_data[UF_SCHEMA].len); memcpy(uri->string + u.field_data[UF_SCHEMA].len, "://", 3); rspamd_strlcpy(uri->string + u.field_data[UF_SCHEMA].len + 3, p + u.field_data[UF_SCHEMA].len + 1, len - 2 - u.field_data[UF_SCHEMA].len); /* Compensate slashes added */ for (int i = UF_SCHEMA + 1; i < UF_MAX; i++) { if (u.field_set & (1 << i)) { u.field_data[i].off += 2; } } } else { uri->string = rspamd_mempool_alloc(pool, len + 1); rspamd_strlcpy(uri->string, p, len + 1); } uri->urllen = len; uri->flags = flags; for (guint i = 0; i < UF_MAX; i++) { if (u.field_set & (1 << i)) { guint shift = u.field_data[i].off; complen = u.field_data[i].len; if (complen >= G_MAXUINT16) { /* Too large component length */ return URI_ERRNO_BAD_FORMAT; } switch (i) { case UF_SCHEMA: uri->protocollen = u.field_data[i].len; break; case UF_HOST: uri->hostshift = shift; uri->hostlen = complen; break; case UF_PATH: uri->datashift = shift; uri->datalen = complen; break; case UF_QUERY: uri->queryshift = shift; uri->querylen = complen; break; case UF_FRAGMENT: uri->fragmentshift = shift; uri->fragmentlen = complen; break; case UF_USERINFO: uri->usershift = shift; uri->userlen = complen; break; default: break; } } } /* Port is 'special' in case of url_parser as it is not a part of UF_* macro logic */ if (u.port != 0) { if (!uri->ext) { uri->ext = rspamd_mempool_alloc0_type(pool, struct rspamd_url_ext); } uri->flags |= RSPAMD_URL_FLAG_HAS_PORT; uri->ext->port = u.port; } if (!uri->hostlen) { return URI_ERRNO_HOST_MISSING; } /* Now decode url symbols */ unquoted_len = rspamd_url_decode(uri->string, uri->string, uri->protocollen); rspamd_url_shift(uri, unquoted_len, UF_SCHEMA); unquoted_len = rspamd_url_decode(rspamd_url_host_unsafe(uri), rspamd_url_host_unsafe(uri), uri->hostlen); rspamd_url_normalise_propagate_flags(pool, rspamd_url_host_unsafe(uri), &unquoted_len, uri->flags); rspamd_url_shift(uri, unquoted_len, UF_HOST); if (rspamd_url_remove_dots(uri)) { uri->flags |= RSPAMD_URL_FLAG_OBSCURED; } if (uri->protocol & (PROTOCOL_HTTP | PROTOCOL_HTTPS | PROTOCOL_MAILTO | PROTOCOL_FTP | PROTOCOL_FILE)) { /* Ensure that hostname starts with something sane (exclude numeric urls) */ const gchar *host = rspamd_url_host_unsafe(uri); if (!(is_domain_start(host[0]) || host[0] == ':')) { return URI_ERRNO_BAD_FORMAT; } } /* Apply nameprep algorithm */ static UStringPrepProfile *nameprep = NULL; UErrorCode uc_err = U_ZERO_ERROR; if (nameprep == NULL) { /* Open and cache profile */ nameprep = usprep_openByType(USPREP_RFC3491_NAMEPREP, &uc_err); g_assert(U_SUCCESS(uc_err)); } UChar *utf16_hostname, *norm_utf16; gint32 utf16_len, norm_utf16_len, norm_utf8_len; UParseError parse_error; utf16_hostname = rspamd_mempool_alloc(pool, uri->hostlen * sizeof(UChar)); struct UConverter *utf8_conv = rspamd_get_utf8_converter(); utf16_len = ucnv_toUChars(utf8_conv, utf16_hostname, uri->hostlen, rspamd_url_host_unsafe(uri), uri->hostlen, &uc_err); if (!U_SUCCESS(uc_err)) { return URI_ERRNO_BAD_FORMAT; } norm_utf16 = rspamd_mempool_alloc(pool, utf16_len * sizeof(UChar)); norm_utf16_len = usprep_prepare(nameprep, utf16_hostname, utf16_len, norm_utf16, utf16_len, USPREP_DEFAULT, &parse_error, &uc_err); if (!U_SUCCESS(uc_err)) { return URI_ERRNO_BAD_FORMAT; } /* Convert back to utf8, sigh... */ norm_utf8_len = ucnv_fromUChars(utf8_conv, rspamd_url_host_unsafe(uri), uri->hostlen, norm_utf16, norm_utf16_len, &uc_err); if (!U_SUCCESS(uc_err)) { return URI_ERRNO_BAD_FORMAT; } /* Final shift of lengths */ rspamd_url_shift(uri, norm_utf8_len, UF_HOST); /* Process data part */ if (uri->datalen) { unquoted_len = rspamd_url_decode(rspamd_url_data_unsafe(uri), rspamd_url_data_unsafe(uri), uri->datalen); rspamd_url_normalise_propagate_flags(pool, rspamd_url_data_unsafe(uri), &unquoted_len, uri->flags); rspamd_url_shift(uri, unquoted_len, UF_PATH); /* We now normalize path */ rspamd_normalize_path_inplace(rspamd_url_data_unsafe(uri), uri->datalen, &unquoted_len); rspamd_url_shift(uri, unquoted_len, UF_PATH); } if (uri->querylen) { unquoted_len = rspamd_url_decode(rspamd_url_query_unsafe(uri), rspamd_url_query_unsafe(uri), uri->querylen); rspamd_url_normalise_propagate_flags(pool, rspamd_url_query_unsafe(uri), &unquoted_len, uri->flags); rspamd_url_shift(uri, unquoted_len, UF_QUERY); } if (uri->fragmentlen) { unquoted_len = rspamd_url_decode(rspamd_url_fragment_unsafe(uri), rspamd_url_fragment_unsafe(uri), uri->fragmentlen); rspamd_url_normalise_propagate_flags(pool, rspamd_url_fragment_unsafe(uri), &unquoted_len, uri->flags); rspamd_url_shift(uri, unquoted_len, UF_FRAGMENT); } rspamd_str_lc(uri->string, uri->protocollen); unquoted_len = rspamd_str_lc_utf8(rspamd_url_host_unsafe(uri), uri->hostlen); rspamd_url_shift(uri, unquoted_len, UF_HOST); if (uri->protocol == PROTOCOL_UNKNOWN) { for (int i = 0; i < G_N_ELEMENTS(rspamd_url_protocols); i++) { if (uri->protocollen == rspamd_url_protocols[i].len) { if (memcmp(uri->string, rspamd_url_protocols[i].name, uri->protocollen) == 0) { uri->protocol = rspamd_url_protocols[i].proto; break; } } } } if (uri->protocol & (PROTOCOL_HTTP | PROTOCOL_HTTPS | PROTOCOL_MAILTO | PROTOCOL_FTP | PROTOCOL_FILE)) { /* Find TLD part */ if (url_scanner->search_trie_full) { rspamd_multipattern_lookup(url_scanner->search_trie_full, rspamd_url_host_unsafe(uri), uri->hostlen, rspamd_tld_trie_callback, uri, NULL); } if (uri->tldlen == 0) { /* * If we have not detected eSLD, but there are no dots in the hostname, * then we should treat the whole hostname as eSLD - a rule of thumb * * We also check that a hostname ends with a permitted character, and all characters are forming * DNS label. We also need to check for a numeric IP within this check. */ const char *dot_pos = memchr(rspamd_url_host_unsafe(uri), '.', uri->hostlen); bool is_whole_hostname_tld = false; if (uri->hostlen > 0 && (dot_pos == NULL || dot_pos == rspamd_url_host_unsafe(uri) + uri->hostlen - 1)) { bool all_chars_domain = true; for (int i = 0; i < uri->hostlen; i++) { if (!is_domain(rspamd_url_host_unsafe(uri)[i])) { all_chars_domain = false; break; } } char last_c = rspamd_url_host_unsafe(uri)[uri->hostlen - 1]; if (all_chars_domain) { /* Also check the last character to be either a dot or alphanumeric character */ if (last_c != '.' && !g_ascii_isalnum(last_c)) { all_chars_domain = false; } } if (all_chars_domain) { /* Additionally check for a numeric IP as we can have some number here... */ rspamd_url_maybe_regenerate_from_ip(uri, pool); if (last_c == '.' && uri->hostlen > 1) { /* Skip the last dot */ uri->tldlen = uri->hostlen - 1; } else { uri->tldlen = uri->hostlen; } uri->tldshift = uri->hostshift; is_whole_hostname_tld = true; } } if (!is_whole_hostname_tld) { if (uri->protocol != PROTOCOL_MAILTO) { if (url_scanner->has_tld_file && !(parse_flags & RSPAMD_URL_PARSE_HREF)) { /* Ignore URL's without TLD if it is not a numeric URL */ if (!rspamd_url_maybe_regenerate_from_ip(uri, pool)) { return URI_ERRNO_TLD_MISSING; } } else { if (!rspamd_url_maybe_regenerate_from_ip(uri, pool)) { /* Assume tld equal to host */ uri->tldshift = uri->hostshift; uri->tldlen = uri->hostlen; } else if (uri->flags & RSPAMD_URL_FLAG_SCHEMALESS) { /* Ignore urls with both no schema and no tld */ return URI_ERRNO_TLD_MISSING; } uri->flags |= RSPAMD_URL_FLAG_NO_TLD; } } else { /* Ignore IP like domains for mailto, as it is really never supported */ return URI_ERRNO_TLD_MISSING; } } } /* Replace stupid '\' with '/' after schema */ if (uri->protocol & (PROTOCOL_HTTP | PROTOCOL_HTTPS | PROTOCOL_FTP) && uri->protocollen > 0 && uri->urllen > uri->protocollen + 2) { gchar *pos = &uri->string[uri->protocollen], *host_start = rspamd_url_host_unsafe(uri); while (pos < host_start) { if (*pos == '\\') { *pos = '/'; uri->flags |= RSPAMD_URL_FLAG_OBSCURED; } pos++; } } } else if (uri->protocol & PROTOCOL_TELEPHONE) { /* We need to normalise phone number: remove all spaces and braces */ rspamd_telephone_normalise_inplace(uri); if (rspamd_url_host_unsafe(uri)[0] == '+') { uri->tldshift = uri->hostshift + 1; uri->tldlen = uri->hostlen - 1; } else { uri->tldshift = uri->hostshift; uri->tldlen = uri->hostlen; } } if (uri->protocol == PROTOCOL_UNKNOWN) { if (!(parse_flags & RSPAMD_URL_PARSE_HREF)) { return URI_ERRNO_INVALID_PROTOCOL; } else { /* Hack, hack, hack */ uri->protocol = PROTOCOL_UNKNOWN; } } return URI_ERRNO_OK; } struct tld_trie_cbdata { const gchar *begin; gsize len; rspamd_ftok_t *out; }; static gint rspamd_tld_trie_find_callback(struct rspamd_multipattern *mp, guint strnum, gint match_start, gint match_pos, const gchar *text, gsize len, void *context) { struct url_matcher *matcher; const gchar *start, *pos, *p; struct tld_trie_cbdata *cbdata = context; gint ndots = 1; matcher = &g_array_index(url_scanner->matchers_full, struct url_matcher, strnum); if (matcher->flags & URL_MATCHER_FLAG_STAR_MATCH) { /* Skip one more tld component */ ndots = 2; } pos = text + match_start; p = pos - 1; start = text; if (*pos != '.' || match_pos != (gint) cbdata->len) { /* Something weird has been found */ if (match_pos != (gint) cbdata->len - 1) { /* Search more */ return 0; } } /* Now we need to find top level domain */ pos = start; while (p >= start && ndots > 0) { if (*p == '.') { ndots--; pos = p + 1; } else { pos = p; } p--; } if (ndots == 0 || p == start - 1) { if (cbdata->begin + cbdata->len - pos > cbdata->out->len) { cbdata->out->begin = pos; cbdata->out->len = cbdata->begin + cbdata->len - pos; } } return 0; } gboolean rspamd_url_find_tld(const gchar *in, gsize inlen, rspamd_ftok_t *out) { struct tld_trie_cbdata cbdata; g_assert(in != NULL); g_assert(out != NULL); g_assert(url_scanner != NULL); cbdata.begin = in; cbdata.len = inlen; cbdata.out = out; out->len = 0; if (url_scanner->search_trie_full) { rspamd_multipattern_lookup(url_scanner->search_trie_full, in, inlen, rspamd_tld_trie_find_callback, &cbdata, NULL); } if (out->len > 0) { return TRUE; } return FALSE; } static const gchar url_braces[] = { '(', ')', '{', '}', '[', ']', '<', '>', '|', '|', '\'', '\''}; static gboolean url_file_start(struct url_callback_data *cb, const gchar *pos, url_match_t *match) { match->m_begin = pos; if (pos > cb->begin) { match->st = *(pos - 1); } else { match->st = '\0'; } return TRUE; } static gboolean url_file_end(struct url_callback_data *cb, const gchar *pos, url_match_t *match) { const gchar *p; gchar stop; guint i; p = pos + strlen(match->pattern); stop = *p; if (*p == '/') { p++; } for (i = 0; i < G_N_ELEMENTS(url_braces) / 2; i += 2) { if (*p == url_braces[i]) { stop = url_braces[i + 1]; break; } } while (p < cb->end && *p != stop && is_urlsafe(*p)) { p++; } if (p == cb->begin) { return FALSE; } match->m_len = p - match->m_begin; return TRUE; } static gboolean url_tld_start(struct url_callback_data *cb, const gchar *pos, url_match_t *match) { const gchar *p = pos; guint processed = 0; static const guint max_shift = 253 + sizeof("https://"); /* Try to find the start of the url by finding any non-urlsafe character or whitespace/punctuation */ while (p >= cb->begin) { if (!is_domain(*p) || g_ascii_isspace(*p) || is_url_start(*p) || p == match->prev_newline_pos) { if (!is_url_start(*p) && !g_ascii_isspace(*p) && p != match->prev_newline_pos) { return FALSE; } if (p != match->prev_newline_pos) { match->st = *p; p++; } else { match->st = '\n'; } if (!g_ascii_isalnum(*p)) { /* Urls cannot start with strange symbols */ return FALSE; } match->m_begin = p; return TRUE; } else if (p == cb->begin && p != pos) { match->st = '\0'; match->m_begin = p; return TRUE; } else if (*p == '.') { if (p == cb->begin) { /* Urls cannot start with a dot */ return FALSE; } if (!g_ascii_isalnum(p[1])) { /* Wrong we have an invalid character after dot */ return FALSE; } } else if (*p == '/') { /* Urls cannot contain '/' in their body */ return FALSE; } p--; processed++; if (processed > max_shift) { /* Too long */ return FALSE; } } return FALSE; } static gboolean url_tld_end(struct url_callback_data *cb, const gchar *pos, url_match_t *match) { const gchar *p; gboolean ret = FALSE; p = pos + match->m_len; if (p == cb->end) { match->m_len = p - match->m_begin; return TRUE; } else if (*p == '/' || *p == ':' || is_url_end(*p) || is_lwsp(*p) || (match->st != '<' && p == match->newline_pos)) { /* Parse arguments, ports by normal way by url default function */ p = match->m_begin; /* Check common prefix */ if (g_ascii_strncasecmp(p, "http://", sizeof("http://") - 1) == 0) { ret = url_web_end(cb, match->m_begin + sizeof("http://") - 1, match); } else { ret = url_web_end(cb, match->m_begin, match); } } else if (*p == '.') { p++; if (p < cb->end) { if (g_ascii_isspace(*p) || *p == '/' || *p == '?' || *p == ':') { ret = url_web_end(cb, match->m_begin, match); } } } if (ret) { /* Check sanity of match found */ if (match->m_begin + match->m_len <= pos) { return FALSE; } } return ret; } static gboolean url_web_start(struct url_callback_data *cb, const gchar *pos, url_match_t *match) { /* Check what we have found */ if (pos > cb->begin) { if (g_ascii_strncasecmp(pos, "www", 3) == 0) { if (!(is_url_start(*(pos - 1)) || g_ascii_isspace(*(pos - 1)) || pos - 1 == match->prev_newline_pos || (*(pos - 1) & 0x80))) { /* Chinese trick */ return FALSE; } } else { guchar prev = *(pos - 1); if (g_ascii_isalnum(prev)) { /* Part of another url */ return FALSE; } } } if (*pos == '.') { /* Urls cannot start with . */ return FALSE; } if (pos > cb->begin) { match->st = *(pos - 1); } else { match->st = '\0'; } match->m_begin = pos; return TRUE; } static gboolean url_web_end(struct url_callback_data *cb, const gchar *pos, url_match_t *match) { const gchar *last = NULL; gint len = cb->end - pos; guint flags = 0; if (match->newline_pos && match->st != '<') { /* We should also limit our match end to the newline */ len = MIN(len, match->newline_pos - pos); } if (rspamd_web_parse(NULL, pos, len, &last, RSPAMD_URL_PARSE_CHECK, &flags) != 0) { return FALSE; } if (last < cb->end && (*last == '>' && last != match->newline_pos)) { /* We need to ensure that url also starts with '>' */ if (match->st != '<') { if (last + 1 < cb->end) { if (g_ascii_isspace(last[1])) { return FALSE; } } else { return FALSE; } } } match->m_len = (last - pos); cb->fin = last + 1; return TRUE; } static gboolean url_email_start(struct url_callback_data *cb, const gchar *pos, url_match_t *match) { if (!match->prefix || match->prefix[0] == '\0') { /* We have mailto:// at the beginning */ match->m_begin = pos; if (pos >= cb->begin + 1) { match->st = *(pos - 1); } else { match->st = '\0'; } } else { /* Just '@' */ /* Check if this match is a part of the previous mailto: email */ if (cb->last_at != NULL && cb->last_at == pos) { cb->last_at = NULL; return FALSE; } else if (pos == cb->begin) { /* Just @ at the start of input */ return FALSE; } match->st = '\0'; } return TRUE; } static gboolean url_email_end(struct url_callback_data *cb, const gchar *pos, url_match_t *match) { const gchar *last = NULL; struct http_parser_url u; gint len = cb->end - pos; guint flags = 0; if (match->newline_pos && match->st != '<') { /* We should also limit our match end to the newline */ len = MIN(len, match->newline_pos - pos); } if (!match->prefix || match->prefix[0] == '\0') { /* We have mailto:// at the beginning */ if (rspamd_mailto_parse(&u, pos, len, &last, RSPAMD_URL_PARSE_CHECK, &flags) != 0) { return FALSE; } if (!(u.field_set & (1 << UF_USERINFO))) { return FALSE; } cb->last_at = match->m_begin + u.field_data[UF_USERINFO].off + u.field_data[UF_USERINFO].len; g_assert(*cb->last_at == '@'); match->m_len = (last - pos); return TRUE; } else { const gchar *c, *p; /* * Here we have just '@', so we need to find both start and end of the * pattern */ g_assert(*pos == '@'); if (pos >= cb->end - 2 || pos < cb->begin + 1) { /* Boundary violation */ return FALSE; } /* Check the next character after `@` */ if (!g_ascii_isalnum(pos[1]) || !g_ascii_isalnum(*(pos - 1))) { return FALSE; } c = pos - 1; while (c > cb->begin) { if (!is_mailsafe(*c)) { break; } if (c == match->prev_newline_pos) { break; } c--; } /* Rewind to the first alphanumeric character */ while (c < pos && !g_ascii_isalnum(*c)) { c++; } /* Find the end of email */ p = pos + 1; while (p < cb->end && is_domain(*p)) { if (p == match->newline_pos) { break; } p++; } /* Rewind it again to avoid bad emails to be detected */ while (p > pos && p < cb->end && !g_ascii_isalnum(*p)) { p--; } if (p < cb->end && g_ascii_isalnum(*p) && (match->newline_pos == NULL || p < match->newline_pos)) { p++; } if (p > c) { match->m_begin = c; match->m_len = p - c; return TRUE; } } return FALSE; } static gboolean url_tel_start(struct url_callback_data *cb, const gchar *pos, url_match_t *match) { match->m_begin = pos; if (pos >= cb->begin + 1) { match->st = *(pos - 1); } else { match->st = '\0'; } return TRUE; } static gboolean url_tel_end(struct url_callback_data *cb, const gchar *pos, url_match_t *match) { const gchar *last = NULL; struct http_parser_url u; gint len = cb->end - pos; guint flags = 0; if (match->newline_pos && match->st != '<') { /* We should also limit our match end to the newline */ len = MIN(len, match->newline_pos - pos); } if (rspamd_telephone_parse(&u, pos, len, &last, RSPAMD_URL_PARSE_CHECK, &flags) != 0) { return FALSE; } if (!(u.field_set & (1 << UF_HOST))) { return FALSE; } match->m_len = (last - pos); return TRUE; } static gboolean rspamd_url_trie_is_match(struct url_matcher *matcher, const gchar *pos, const gchar *end, const gchar *newline_pos) { if (matcher->flags & URL_MATCHER_FLAG_TLD_MATCH) { /* Immediately check pos for valid chars */ if (pos < end) { if (pos != newline_pos && !g_ascii_isspace(*pos) && *pos != '/' && *pos != '?' && *pos != ':' && !is_url_end(*pos)) { if (*pos == '.') { /* We allow . at the end of the domain however */ pos++; if (pos < end) { if (!g_ascii_isspace(*pos) && *pos != '/' && *pos != '?' && *pos != ':' && !is_url_end(*pos)) { return FALSE; } } } else { return FALSE; } } } } return TRUE; } static gint rspamd_url_trie_callback(struct rspamd_multipattern *mp, guint strnum, gint match_start, gint match_pos, const gchar *text, gsize len, void *context) { struct url_matcher *matcher; url_match_t m; const gchar *pos, *newline_pos = NULL; struct url_callback_data *cb = context; pos = text + match_pos; if (cb->fin > pos) { /* Already seen */ return 0; } matcher = &g_array_index(cb->matchers, struct url_matcher, strnum); if ((matcher->flags & URL_MATCHER_FLAG_NOHTML) && cb->how == RSPAMD_URL_FIND_STRICT) { /* Do not try to match non-html like urls in html texts */ return 0; } memset(&m, 0, sizeof(m)); m.m_begin = text + match_start; m.m_len = match_pos - match_start; if (cb->newlines && cb->newlines->len > 0) { newline_pos = g_ptr_array_index(cb->newlines, cb->newline_idx); while (pos > newline_pos && cb->newline_idx < cb->newlines->len) { cb->newline_idx++; newline_pos = g_ptr_array_index(cb->newlines, cb->newline_idx); } if (pos > newline_pos) { newline_pos = NULL; } if (cb->newline_idx > 0) { m.prev_newline_pos = g_ptr_array_index(cb->newlines, cb->newline_idx - 1); } } if (!rspamd_url_trie_is_match(matcher, pos, cb->end, newline_pos)) { return 0; } m.pattern = matcher->pattern; m.prefix = matcher->prefix; m.add_prefix = FALSE; m.newline_pos = newline_pos; pos = cb->begin + match_start; if (matcher->start(cb, pos, &m) && matcher->end(cb, pos, &m)) { if (m.add_prefix || matcher->prefix[0] != '\0') { cb->len = m.m_len + strlen(matcher->prefix); cb->url_str = rspamd_mempool_alloc(cb->pool, cb->len + 1); cb->len = rspamd_snprintf(cb->url_str, cb->len + 1, "%s%*s", m.prefix, (gint) m.m_len, m.m_begin); cb->prefix_added = TRUE; } else { cb->url_str = rspamd_mempool_alloc(cb->pool, m.m_len + 1); rspamd_strlcpy(cb->url_str, m.m_begin, m.m_len + 1); } cb->start = m.m_begin; if (pos > cb->fin) { cb->fin = pos; } return 1; } else { cb->url_str = NULL; } /* Continue search */ return 0; } gboolean rspamd_url_find(rspamd_mempool_t *pool, const gchar *begin, gsize len, gchar **url_str, enum rspamd_url_find_type how, goffset *url_pos, gboolean *prefix_added) { struct url_callback_data cb; gint ret; memset(&cb, 0, sizeof(cb)); cb.begin = begin; cb.end = begin + len; cb.how = how; cb.pool = pool; if (how == RSPAMD_URL_FIND_ALL) { if (url_scanner->search_trie_full) { cb.matchers = url_scanner->matchers_full; ret = rspamd_multipattern_lookup(url_scanner->search_trie_full, begin, len, rspamd_url_trie_callback, &cb, NULL); } else { cb.matchers = url_scanner->matchers_strict; ret = rspamd_multipattern_lookup(url_scanner->search_trie_strict, begin, len, rspamd_url_trie_callback, &cb, NULL); } } else { cb.matchers = url_scanner->matchers_strict; ret = rspamd_multipattern_lookup(url_scanner->search_trie_strict, begin, len, rspamd_url_trie_callback, &cb, NULL); } if (ret) { if (url_str) { *url_str = cb.url_str; } if (url_pos) { *url_pos = cb.start - begin; } if (prefix_added) { *prefix_added = cb.prefix_added; } return TRUE; } return FALSE; } static gint rspamd_url_trie_generic_callback_common(struct rspamd_multipattern *mp, guint strnum, gint match_start, gint match_pos, const gchar *text, gsize len, void *context, gboolean multiple) { struct rspamd_url *url; struct url_matcher *matcher; url_match_t m; const gchar *pos, *newline_pos = NULL; struct url_callback_data *cb = context; gint rc; rspamd_mempool_t *pool; pos = text + match_pos; if (cb->fin > pos) { /* Already seen */ return 0; } matcher = &g_array_index(cb->matchers, struct url_matcher, strnum); pool = cb->pool; if ((matcher->flags & URL_MATCHER_FLAG_NOHTML) && cb->how == RSPAMD_URL_FIND_STRICT) { /* Do not try to match non-html like urls in html texts, continue matching */ return 0; } memset(&m, 0, sizeof(m)); /* Find the next newline after our pos */ if (cb->newlines && cb->newlines->len > 0) { newline_pos = g_ptr_array_index(cb->newlines, cb->newline_idx); while (pos > newline_pos && cb->newline_idx < cb->newlines->len - 1) { cb->newline_idx++; newline_pos = g_ptr_array_index(cb->newlines, cb->newline_idx); } if (pos > newline_pos) { newline_pos = NULL; } if (cb->newline_idx > 0) { m.prev_newline_pos = g_ptr_array_index(cb->newlines, cb->newline_idx - 1); } } if (!rspamd_url_trie_is_match(matcher, pos, text + len, newline_pos)) { /* Mismatch, continue */ return 0; } pos = cb->begin + match_start; m.pattern = matcher->pattern; m.prefix = matcher->prefix; m.add_prefix = FALSE; m.m_begin = text + match_start; m.m_len = match_pos - match_start; m.newline_pos = newline_pos; if (matcher->start(cb, pos, &m) && matcher->end(cb, pos, &m)) { if (m.add_prefix || matcher->prefix[0] != '\0') { cb->len = m.m_len + strlen(matcher->prefix); cb->url_str = rspamd_mempool_alloc(cb->pool, cb->len + 1); cb->len = rspamd_snprintf(cb->url_str, cb->len + 1, "%s%*s", m.prefix, (gint) m.m_len, m.m_begin); cb->prefix_added = TRUE; } else { cb->url_str = rspamd_mempool_alloc(cb->pool, m.m_len + 1); cb->len = rspamd_strlcpy(cb->url_str, m.m_begin, m.m_len + 1); } cb->start = m.m_begin; if (pos > cb->fin) { cb->fin = pos; } url = rspamd_mempool_alloc0(pool, sizeof(struct rspamd_url)); g_strstrip(cb->url_str); rc = rspamd_url_parse(url, cb->url_str, strlen(cb->url_str), pool, RSPAMD_URL_PARSE_TEXT); if (rc == URI_ERRNO_OK && url->hostlen > 0) { if (cb->prefix_added) { url->flags |= RSPAMD_URL_FLAG_SCHEMALESS; cb->prefix_added = FALSE; } if (cb->func) { if (!cb->func(url, cb->start - text, (m.m_begin + m.m_len) - text, cb->funcd)) { /* We need to stop here in any case! */ return -1; } } } else if (rc != URI_ERRNO_OK) { msg_debug_pool_check("extract of url '%s' failed: %s", cb->url_str, rspamd_url_strerror(rc)); } } else { cb->url_str = NULL; /* Continue search if no pattern has been found */ return 0; } /* Continue search if required (return 0 means continue) */ return !multiple; } static gint rspamd_url_trie_generic_callback_multiple(struct rspamd_multipattern *mp, guint strnum, gint match_start, gint match_pos, const gchar *text, gsize len, void *context) { return rspamd_url_trie_generic_callback_common(mp, strnum, match_start, match_pos, text, len, context, TRUE); } static gint rspamd_url_trie_generic_callback_single(struct rspamd_multipattern *mp, guint strnum, gint match_start, gint match_pos, const gchar *text, gsize len, void *context) { return rspamd_url_trie_generic_callback_common(mp, strnum, match_start, match_pos, text, len, context, FALSE); } struct rspamd_url_mimepart_cbdata { struct rspamd_task *task; struct rspamd_mime_text_part *part; gsize url_len; uint16_t *cur_url_order; /* Global ordering */ uint16_t cur_part_order; /* Per part ordering */ }; static gboolean rspamd_url_query_callback(struct rspamd_url *url, gsize start_offset, gsize end_offset, gpointer ud) { struct rspamd_url_mimepart_cbdata *cbd = (struct rspamd_url_mimepart_cbdata *) ud; struct rspamd_task *task; task = cbd->task; if (url->protocol == PROTOCOL_MAILTO) { if (url->userlen == 0) { return FALSE; } } /* Also check max urls */ if (cbd->task->cfg && cbd->task->cfg->max_urls > 0) { if (kh_size(MESSAGE_FIELD(task, urls)) > cbd->task->cfg->max_urls) { msg_err_task("part has too many URLs, we cannot process more: " "%d urls extracted ", (guint) kh_size(MESSAGE_FIELD(task, urls))); return FALSE; } } url->flags |= RSPAMD_URL_FLAG_QUERY; if (rspamd_url_set_add_or_increase(MESSAGE_FIELD(task, urls), url, false)) { if (cbd->part && cbd->part->mime_part->urls) { g_ptr_array_add(cbd->part->mime_part->urls, url); } url->part_order = cbd->cur_part_order++; if (cbd->cur_url_order) { url->order = (*cbd->cur_url_order)++; } } return TRUE; } static gboolean rspamd_url_text_part_callback(struct rspamd_url *url, gsize start_offset, gsize end_offset, gpointer ud) { struct rspamd_url_mimepart_cbdata *cbd = (struct rspamd_url_mimepart_cbdata *) ud; struct rspamd_process_exception *ex; struct rspamd_task *task; task = cbd->task; ex = rspamd_mempool_alloc0(task->task_pool, sizeof(struct rspamd_process_exception)); ex->pos = start_offset; ex->len = end_offset - start_offset; ex->type = RSPAMD_EXCEPTION_URL; ex->ptr = url; cbd->url_len += ex->len; if (cbd->part->utf_stripped_content && cbd->url_len > cbd->part->utf_stripped_content->len * 10) { /* Absurd case, stop here now */ msg_err_task("part has too many URLs, we cannot process more: %z url len; " "%d stripped content length", cbd->url_len, cbd->part->utf_stripped_content->len); return FALSE; } if (url->protocol == PROTOCOL_MAILTO) { if (url->userlen == 0) { return FALSE; } } /* Also check max urls */ if (cbd->task->cfg && cbd->task->cfg->max_urls > 0) { if (kh_size(MESSAGE_FIELD(task, urls)) > cbd->task->cfg->max_urls) { msg_err_task("part has too many URLs, we cannot process more: " "%d urls extracted ", (guint) kh_size(MESSAGE_FIELD(task, urls))); return FALSE; } } url->flags |= RSPAMD_URL_FLAG_FROM_TEXT; if (rspamd_url_set_add_or_increase(MESSAGE_FIELD(task, urls), url, false) && cbd->part->mime_part->urls) { url->part_order = cbd->cur_part_order++; if (cbd->cur_url_order) { url->order = (*cbd->cur_url_order)++; } g_ptr_array_add(cbd->part->mime_part->urls, url); } cbd->part->exceptions = g_list_prepend( cbd->part->exceptions, ex); /* We also search the query for additional url inside */ if (url->querylen > 0) { rspamd_url_find_multiple(task->task_pool, rspamd_url_query_unsafe(url), url->querylen, RSPAMD_URL_FIND_ALL, NULL, rspamd_url_query_callback, cbd); } return TRUE; } void rspamd_url_text_extract(rspamd_mempool_t *pool, struct rspamd_task *task, struct rspamd_mime_text_part *part, uint16_t *cur_url_order, enum rspamd_url_find_type how) { struct rspamd_url_mimepart_cbdata mcbd; if (part->utf_stripped_content == NULL || part->utf_stripped_content->len == 0) { msg_warn_task("got empty text part"); return; } mcbd.task = task; mcbd.part = part; mcbd.url_len = 0; mcbd.cur_url_order = cur_url_order; mcbd.cur_part_order = 0; rspamd_url_find_multiple(task->task_pool, part->utf_stripped_content->data, part->utf_stripped_content->len, how, part->newlines, rspamd_url_text_part_callback, &mcbd); } void rspamd_url_find_multiple(rspamd_mempool_t *pool, const gchar *in, gsize inlen, enum rspamd_url_find_type how, GPtrArray *nlines, url_insert_function func, gpointer ud) { struct url_callback_data cb; g_assert(in != NULL); if (inlen == 0) { inlen = strlen(in); } memset(&cb, 0, sizeof(cb)); cb.begin = in; cb.end = in + inlen; cb.how = how; cb.pool = pool; cb.funcd = ud; cb.func = func; cb.newlines = nlines; if (how == RSPAMD_URL_FIND_ALL) { if (url_scanner->search_trie_full) { cb.matchers = url_scanner->matchers_full; rspamd_multipattern_lookup(url_scanner->search_trie_full, in, inlen, rspamd_url_trie_generic_callback_multiple, &cb, NULL); } else { cb.matchers = url_scanner->matchers_strict; rspamd_multipattern_lookup(url_scanner->search_trie_strict, in, inlen, rspamd_url_trie_generic_callback_multiple, &cb, NULL); } } else { cb.matchers = url_scanner->matchers_strict; rspamd_multipattern_lookup(url_scanner->search_trie_strict, in, inlen, rspamd_url_trie_generic_callback_multiple, &cb, NULL); } } void rspamd_url_find_single(rspamd_mempool_t *pool, const gchar *in, gsize inlen, enum rspamd_url_find_type how, url_insert_function func, gpointer ud) { struct url_callback_data cb; g_assert(in != NULL); if (inlen == 0) { inlen = strlen(in); } /* * We might have a situation when we need to parse URLs on config file * parsing, but there is no valid url_scanner loaded. Hence, we just load * some defaults and it should be fine... */ if (url_scanner == NULL) { rspamd_url_init(NULL); } memset(&cb, 0, sizeof(cb)); cb.begin = in; cb.end = in + inlen; cb.how = how; cb.pool = pool; cb.funcd = ud; cb.func = func; if (how == RSPAMD_URL_FIND_ALL) { if (url_scanner->search_trie_full) { cb.matchers = url_scanner->matchers_full; rspamd_multipattern_lookup(url_scanner->search_trie_full, in, inlen, rspamd_url_trie_generic_callback_single, &cb, NULL); } else { cb.matchers = url_scanner->matchers_strict; rspamd_multipattern_lookup(url_scanner->search_trie_strict, in, inlen, rspamd_url_trie_generic_callback_single, &cb, NULL); } } else { cb.matchers = url_scanner->matchers_strict; rspamd_multipattern_lookup(url_scanner->search_trie_strict, in, inlen, rspamd_url_trie_generic_callback_single, &cb, NULL); } } gboolean rspamd_url_task_subject_callback(struct rspamd_url *url, gsize start_offset, gsize end_offset, gpointer ud) { struct rspamd_task *task = ud; gchar *url_str = NULL; struct rspamd_url *query_url; gint rc; gboolean prefix_added; /* It is just a displayed URL, we should not check it for certain things */ url->flags |= RSPAMD_URL_FLAG_HTML_DISPLAYED | RSPAMD_URL_FLAG_SUBJECT; if (url->protocol == PROTOCOL_MAILTO) { if (url->userlen == 0) { return FALSE; } } rspamd_url_set_add_or_increase(MESSAGE_FIELD(task, urls), url, false); /* We also search the query for additional url inside */ if (url->querylen > 0) { if (rspamd_url_find(task->task_pool, rspamd_url_query_unsafe(url), url->querylen, &url_str, RSPAMD_URL_FIND_ALL, NULL, &prefix_added)) { query_url = rspamd_mempool_alloc0(task->task_pool, sizeof(struct rspamd_url)); rc = rspamd_url_parse(query_url, url_str, strlen(url_str), task->task_pool, RSPAMD_URL_PARSE_TEXT); if (rc == URI_ERRNO_OK && url->hostlen > 0) { msg_debug_task("found url %s in query of url" " %*s", url_str, url->querylen, rspamd_url_query_unsafe(url)); if (prefix_added) { query_url->flags |= RSPAMD_URL_FLAG_SCHEMALESS; } if (query_url->protocol == PROTOCOL_MAILTO) { if (query_url->userlen == 0) { return TRUE; } } rspamd_url_set_add_or_increase(MESSAGE_FIELD(task, urls), query_url, false); } } } return TRUE; } static inline khint_t rspamd_url_hash(struct rspamd_url *url) { if (url->urllen > 0) { return (khint_t) rspamd_cryptobox_fast_hash(url->string, url->urllen, rspamd_hash_seed()); } return 0; } static inline khint_t rspamd_url_host_hash(struct rspamd_url *url) { if (url->hostlen > 0) { return (khint_t) rspamd_cryptobox_fast_hash(rspamd_url_host_unsafe(url), url->hostlen, rspamd_hash_seed()); } return 0; } /* Compare two emails for building emails tree */ static inline bool rspamd_emails_cmp(struct rspamd_url *u1, struct rspamd_url *u2) { gint r; if (u1->hostlen != u2->hostlen || u1->hostlen == 0) { return FALSE; } else { if ((r = rspamd_lc_cmp(rspamd_url_host_unsafe(u1), rspamd_url_host_unsafe(u2), u1->hostlen)) == 0) { if (u1->userlen != u2->userlen || u1->userlen == 0) { return FALSE; } else { return (rspamd_lc_cmp(rspamd_url_user_unsafe(u1), rspamd_url_user_unsafe(u2), u1->userlen) == 0); } } else { return r == 0; } } return FALSE; } static inline bool rspamd_urls_cmp(struct rspamd_url *u1, struct rspamd_url *u2) { int r = 0; if (u1->protocol != u2->protocol || u1->urllen != u2->urllen) { return false; } else { if (u1->protocol & PROTOCOL_MAILTO) { return rspamd_emails_cmp(u1, u2); } r = memcmp(u1->string, u2->string, u1->urllen); } return r == 0; } static inline bool rspamd_urls_host_cmp(struct rspamd_url *u1, struct rspamd_url *u2) { int r = 0; if (u1->hostlen != u2->hostlen) { return false; } else { r = memcmp(rspamd_url_host_unsafe(u1), rspamd_url_host_unsafe(u2), u1->hostlen); } return r == 0; } gsize rspamd_url_decode(gchar *dst, const gchar *src, gsize size) { gchar *d, ch, c, decoded; const gchar *s; enum { sw_usual = 0, sw_quoted, sw_quoted_second } state; d = dst; s = src; state = 0; decoded = 0; while (size--) { ch = *s++; switch (state) { case sw_usual: if (ch == '%') { state = sw_quoted; break; } else if (ch == '+') { *d++ = ' '; } else { *d++ = ch; } break; case sw_quoted: if (ch >= '0' && ch <= '9') { decoded = (ch - '0'); state = sw_quoted_second; break; } c = (ch | 0x20); if (c >= 'a' && c <= 'f') { decoded = (c - 'a' + 10); state = sw_quoted_second; break; } /* the invalid quoted character */ state = sw_usual; *d++ = ch; break; case sw_quoted_second: state = sw_usual; if (ch >= '0' && ch <= '9') { ch = ((decoded << 4) + ch - '0'); *d++ = ch; break; } c = (u_char) (ch | 0x20); if (c >= 'a' && c <= 'f') { ch = ((decoded << 4) + c - 'a' + 10); *d++ = ch; break; } /* the invalid quoted character */ break; } } return (d - dst); } enum rspamd_url_char_class { RSPAMD_URL_UNRESERVED = (1 << 0), RSPAMD_URL_SUBDELIM = (1 << 1), RSPAMD_URL_PATHSAFE = (1 << 2), RSPAMD_URL_QUERYSAFE = (1 << 3), RSPAMD_URL_FRAGMENTSAFE = (1 << 4), RSPAMD_URL_HOSTSAFE = (1 << 5), RSPAMD_URL_USERSAFE = (1 << 6), }; #define RSPAMD_URL_FLAGS_HOSTSAFE (RSPAMD_URL_UNRESERVED | RSPAMD_URL_HOSTSAFE | RSPAMD_URL_SUBDELIM) #define RSPAMD_URL_FLAGS_USERSAFE (RSPAMD_URL_UNRESERVED | RSPAMD_URL_USERSAFE | RSPAMD_URL_SUBDELIM) #define RSPAMD_URL_FLAGS_PATHSAFE (RSPAMD_URL_UNRESERVED | RSPAMD_URL_PATHSAFE | RSPAMD_URL_SUBDELIM) #define RSPAMD_URL_FLAGS_QUERYSAFE (RSPAMD_URL_UNRESERVED | RSPAMD_URL_QUERYSAFE | RSPAMD_URL_SUBDELIM) #define RSPAMD_URL_FLAGS_FRAGMENTSAFE (RSPAMD_URL_UNRESERVED | RSPAMD_URL_FRAGMENTSAFE | RSPAMD_URL_SUBDELIM) static const unsigned char rspamd_url_encoding_classes[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 /* */, RSPAMD_URL_SUBDELIM /* ! */, 0 /* " */, 0 /* # */, RSPAMD_URL_SUBDELIM /* $ */, 0 /* % */, RSPAMD_URL_SUBDELIM /* & */, RSPAMD_URL_SUBDELIM /* ' */, RSPAMD_URL_SUBDELIM /* ( */, RSPAMD_URL_SUBDELIM /* ) */, RSPAMD_URL_SUBDELIM /* * */, RSPAMD_URL_SUBDELIM /* + */, RSPAMD_URL_SUBDELIM /* , */, RSPAMD_URL_UNRESERVED /* - */, RSPAMD_URL_UNRESERVED /* . */, RSPAMD_URL_PATHSAFE | RSPAMD_URL_QUERYSAFE | RSPAMD_URL_FRAGMENTSAFE /* / */, RSPAMD_URL_UNRESERVED /* 0 */, RSPAMD_URL_UNRESERVED /* 1 */, RSPAMD_URL_UNRESERVED /* 2 */, RSPAMD_URL_UNRESERVED /* 3 */, RSPAMD_URL_UNRESERVED /* 4 */, RSPAMD_URL_UNRESERVED /* 5 */, RSPAMD_URL_UNRESERVED /* 6 */, RSPAMD_URL_UNRESERVED /* 7 */, RSPAMD_URL_UNRESERVED /* 8 */, RSPAMD_URL_UNRESERVED /* 9 */, RSPAMD_URL_USERSAFE | RSPAMD_URL_HOSTSAFE | RSPAMD_URL_PATHSAFE | RSPAMD_URL_QUERYSAFE | RSPAMD_URL_FRAGMENTSAFE /* : */, RSPAMD_URL_SUBDELIM /* ; */, 0 /* < */, RSPAMD_URL_SUBDELIM /* = */, 0 /* > */, RSPAMD_URL_QUERYSAFE | RSPAMD_URL_FRAGMENTSAFE /* ? */, RSPAMD_URL_PATHSAFE | RSPAMD_URL_QUERYSAFE | RSPAMD_URL_FRAGMENTSAFE /* @ */, RSPAMD_URL_UNRESERVED /* A */, RSPAMD_URL_UNRESERVED /* B */, RSPAMD_URL_UNRESERVED /* C */, RSPAMD_URL_UNRESERVED /* D */, RSPAMD_URL_UNRESERVED /* E */, RSPAMD_URL_UNRESERVED /* F */, RSPAMD_URL_UNRESERVED /* G */, RSPAMD_URL_UNRESERVED /* H */, RSPAMD_URL_UNRESERVED /* I */, RSPAMD_URL_UNRESERVED /* J */, RSPAMD_URL_UNRESERVED /* K */, RSPAMD_URL_UNRESERVED /* L */, RSPAMD_URL_UNRESERVED /* M */, RSPAMD_URL_UNRESERVED /* N */, RSPAMD_URL_UNRESERVED /* O */, RSPAMD_URL_UNRESERVED /* P */, RSPAMD_URL_UNRESERVED /* Q */, RSPAMD_URL_UNRESERVED /* R */, RSPAMD_URL_UNRESERVED /* S */, RSPAMD_URL_UNRESERVED /* T */, RSPAMD_URL_UNRESERVED /* U */, RSPAMD_URL_UNRESERVED /* V */, RSPAMD_URL_UNRESERVED /* W */, RSPAMD_URL_UNRESERVED /* X */, RSPAMD_URL_UNRESERVED /* Y */, RSPAMD_URL_UNRESERVED /* Z */, RSPAMD_URL_HOSTSAFE /* [ */, 0 /* \ */, RSPAMD_URL_HOSTSAFE /* ] */, 0 /* ^ */, RSPAMD_URL_UNRESERVED /* _ */, 0 /* ` */, RSPAMD_URL_UNRESERVED /* a */, RSPAMD_URL_UNRESERVED /* b */, RSPAMD_URL_UNRESERVED /* c */, RSPAMD_URL_UNRESERVED /* d */, RSPAMD_URL_UNRESERVED /* e */, RSPAMD_URL_UNRESERVED /* f */, RSPAMD_URL_UNRESERVED /* g */, RSPAMD_URL_UNRESERVED /* h */, RSPAMD_URL_UNRESERVED /* i */, RSPAMD_URL_UNRESERVED /* j */, RSPAMD_URL_UNRESERVED /* k */, RSPAMD_URL_UNRESERVED /* l */, RSPAMD_URL_UNRESERVED /* m */, RSPAMD_URL_UNRESERVED /* n */, RSPAMD_URL_UNRESERVED /* o */, RSPAMD_URL_UNRESERVED /* p */, RSPAMD_URL_UNRESERVED /* q */, RSPAMD_URL_UNRESERVED /* r */, RSPAMD_URL_UNRESERVED /* s */, RSPAMD_URL_UNRESERVED /* t */, RSPAMD_URL_UNRESERVED /* u */, RSPAMD_URL_UNRESERVED /* v */, RSPAMD_URL_UNRESERVED /* w */, RSPAMD_URL_UNRESERVED /* x */, RSPAMD_URL_UNRESERVED /* y */, RSPAMD_URL_UNRESERVED /* z */, 0 /* { */, 0 /* | */, 0 /* } */, RSPAMD_URL_UNRESERVED /* ~ */, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; #define CHECK_URL_COMPONENT(beg, len, flags) \ do { \ for (i = 0; i < (len); i++) { \ if ((rspamd_url_encoding_classes[(guchar) (beg)[i]] & (flags)) == 0) { \ dlen += 2; \ } \ } \ } while (0) #define ENCODE_URL_COMPONENT(beg, len, flags) \ do { \ for (i = 0; i < (len) && dend > d; i++) { \ if ((rspamd_url_encoding_classes[(guchar) (beg)[i]] & (flags)) == 0) { \ *d++ = '%'; \ *d++ = hexdigests[(guchar) ((beg)[i] >> 4) & 0xf]; \ *d++ = hexdigests[(guchar) (beg)[i] & 0xf]; \ } \ else { \ *d++ = (beg)[i]; \ } \ } \ } while (0) const gchar * rspamd_url_encode(struct rspamd_url *url, gsize *pdlen, rspamd_mempool_t *pool) { guchar *dest, *d, *dend; static const gchar hexdigests[16] = "0123456789ABCDEF"; guint i; gsize dlen = 0; g_assert(pdlen != NULL && url != NULL && pool != NULL); CHECK_URL_COMPONENT(rspamd_url_host_unsafe(url), url->hostlen, RSPAMD_URL_FLAGS_HOSTSAFE); CHECK_URL_COMPONENT(rspamd_url_user_unsafe(url), url->userlen, RSPAMD_URL_FLAGS_USERSAFE); CHECK_URL_COMPONENT(rspamd_url_data_unsafe(url), url->datalen, RSPAMD_URL_FLAGS_PATHSAFE); CHECK_URL_COMPONENT(rspamd_url_query_unsafe(url), url->querylen, RSPAMD_URL_FLAGS_QUERYSAFE); CHECK_URL_COMPONENT(rspamd_url_fragment_unsafe(url), url->fragmentlen, RSPAMD_URL_FLAGS_FRAGMENTSAFE); if (dlen == 0) { *pdlen = url->urllen; return url->string; } /* Need to encode */ dlen += url->urllen + sizeof("telephone://"); /* Protocol hack */ dest = rspamd_mempool_alloc(pool, dlen + 1); d = dest; dend = d + dlen; if (url->protocollen > 0) { if (!(url->protocol & PROTOCOL_UNKNOWN)) { const gchar *known_proto = rspamd_url_protocol_name(url->protocol); d += rspamd_snprintf((gchar *) d, dend - d, "%s://", known_proto); } else { d += rspamd_snprintf((gchar *) d, dend - d, "%*s://", (gint) url->protocollen, url->string); } } else { d += rspamd_snprintf((gchar *) d, dend - d, "http://"); } if (url->userlen > 0) { ENCODE_URL_COMPONENT(rspamd_url_user_unsafe(url), url->userlen, RSPAMD_URL_FLAGS_USERSAFE); *d++ = '@'; } ENCODE_URL_COMPONENT(rspamd_url_host_unsafe(url), url->hostlen, RSPAMD_URL_FLAGS_HOSTSAFE); if (url->datalen > 0) { *d++ = '/'; ENCODE_URL_COMPONENT(rspamd_url_data_unsafe(url), url->datalen, RSPAMD_URL_FLAGS_PATHSAFE); } if (url->querylen > 0) { *d++ = '?'; ENCODE_URL_COMPONENT(rspamd_url_query_unsafe(url), url->querylen, RSPAMD_URL_FLAGS_QUERYSAFE); } if (url->fragmentlen > 0) { *d++ = '#'; ENCODE_URL_COMPONENT(rspamd_url_fragment_unsafe(url), url->fragmentlen, RSPAMD_URL_FLAGS_FRAGMENTSAFE); } *pdlen = (d - dest); return (const gchar *) dest; } gboolean rspamd_url_is_domain(int c) { return is_domain((guchar) c); } const gchar * rspamd_url_protocol_name(enum rspamd_url_protocol proto) { const gchar *ret = "unknown"; switch (proto) { case PROTOCOL_HTTP: ret = "http"; break; case PROTOCOL_HTTPS: ret = "https"; break; case PROTOCOL_FTP: ret = "ftp"; break; case PROTOCOL_FILE: ret = "file"; break; case PROTOCOL_MAILTO: ret = "mailto"; break; case PROTOCOL_TELEPHONE: ret = "telephone"; break; default: break; } return ret; } enum rspamd_url_protocol rspamd_url_protocol_from_string(const gchar *str) { enum rspamd_url_protocol ret = PROTOCOL_UNKNOWN; if (strcmp(str, "http") == 0) { ret = PROTOCOL_HTTP; } else if (strcmp(str, "https") == 0) { ret = PROTOCOL_HTTPS; } else if (strcmp(str, "mailto") == 0) { ret = PROTOCOL_MAILTO; } else if (strcmp(str, "ftp") == 0) { ret = PROTOCOL_FTP; } else if (strcmp(str, "file") == 0) { ret = PROTOCOL_FILE; } else if (strcmp(str, "telephone") == 0) { ret = PROTOCOL_TELEPHONE; } return ret; } bool rspamd_url_set_add_or_increase(khash_t(rspamd_url_hash) * set, struct rspamd_url *u, bool enforce_replace) { khiter_t k; gint r; k = kh_get(rspamd_url_hash, set, u); if (k != kh_end(set)) { /* Existing url */ struct rspamd_url *ex = kh_key(set, k); #define SUSPICIOUS_URL_FLAGS (RSPAMD_URL_FLAG_PHISHED | RSPAMD_URL_FLAG_OBSCURED | RSPAMD_URL_FLAG_ZW_SPACES) if (enforce_replace) { kh_key(set, k) = u; u->count++; } else { if (u->flags & SUSPICIOUS_URL_FLAGS) { if (!(ex->flags & SUSPICIOUS_URL_FLAGS)) { /* Propagate new url to an old one */ kh_key(set, k) = u; u->count++; } else { ex->count++; } } else { ex->count++; } } return false; } else { k = kh_put(rspamd_url_hash, set, u, &r); } return true; } struct rspamd_url * rspamd_url_set_add_or_return(khash_t(rspamd_url_hash) * set, struct rspamd_url *u) { khiter_t k; gint r; if (set) { k = kh_get(rspamd_url_hash, set, u); if (k != kh_end(set)) { return kh_key(set, k); } else { k = kh_put(rspamd_url_hash, set, u, &r); return kh_key(set, k); } } return NULL; } bool rspamd_url_host_set_add(khash_t(rspamd_url_host_hash) * set, struct rspamd_url *u) { gint r; if (set) { kh_put(rspamd_url_host_hash, set, u, &r); if (r == 0) { return false; } return true; } return false; } bool rspamd_url_set_has(khash_t(rspamd_url_hash) * set, struct rspamd_url *u) { khiter_t k; if (set) { k = kh_get(rspamd_url_hash, set, u); if (k == kh_end(set)) { return false; } return true; } return false; } bool rspamd_url_host_set_has(khash_t(rspamd_url_host_hash) * set, struct rspamd_url *u) { khiter_t k; if (set) { k = kh_get(rspamd_url_host_hash, set, u); if (k == kh_end(set)) { return false; } return true; } return false; } bool rspamd_url_flag_from_string(const gchar *str, gint *flag) { gint h = rspamd_cryptobox_fast_hash_specific(RSPAMD_CRYPTOBOX_HASHFAST_INDEPENDENT, str, strlen(str), 0); for (int i = 0; i < G_N_ELEMENTS(url_flag_names); i++) { if (url_flag_names[i].hash == h) { *flag |= url_flag_names[i].flag; return true; } } return false; } const gchar * rspamd_url_flag_to_string(int flag) { for (int i = 0; i < G_N_ELEMENTS(url_flag_names); i++) { if (url_flag_names[i].flag & flag) { return url_flag_names[i].name; } } return NULL; } inline int rspamd_url_cmp(const struct rspamd_url *u1, const struct rspamd_url *u2) { int min_len = MIN(u1->urllen, u2->urllen); int r; if (u1->protocol != u2->protocol) { return u1->protocol - u2->protocol; } if (u1->protocol & PROTOCOL_MAILTO) { /* Emails specialisation (hosts must be compared in a case insensitive matter */ min_len = MIN(u1->hostlen, u2->hostlen); if ((r = rspamd_lc_cmp(rspamd_url_host_unsafe(u1), rspamd_url_host_unsafe(u2), min_len)) == 0) { if (u1->hostlen == u2->hostlen) { if (u1->userlen != u2->userlen || u1->userlen == 0) { r = (int) u1->userlen - (int) u2->userlen; } else { r = memcmp(rspamd_url_user_unsafe(u1), rspamd_url_user_unsafe(u2), u1->userlen); } } else { r = u1->hostlen - u2->hostlen; } } } else { if (u1->urllen != u2->urllen) { /* Different length, compare common part and then compare length */ r = memcmp(u1->string, u2->string, min_len); if (r == 0) { r = u1->urllen - u2->urllen; } } else { /* Equal length */ r = memcmp(u1->string, u2->string, u1->urllen); } } return r; } int rspamd_url_cmp_qsort(const void *_u1, const void *_u2) { const struct rspamd_url *u1 = *(struct rspamd_url **) _u1, *u2 = *(struct rspamd_url **) _u2; return rspamd_url_cmp(u1, u2); }